/**
 * @license
 * Copyright 2022-2024 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { WindowCovering } from "#clusters/window-covering";
import { ImplementationError, isDeepEqual, Logger, MaybePromise } from "#general";
import { ClusterType, StatusCode, StatusResponseError } from "#types";
import { WindowCoveringBehavior } from "./WindowCoveringBehavior.js";
const logger = Logger.get("WindowCoveringServer");
const WindowCoveringServerBase = WindowCoveringBehavior.with(
  WindowCovering.Feature.Lift,
  WindowCovering.Feature.Tilt,
  WindowCovering.Feature.PositionAwareLift,
  WindowCovering.Feature.PositionAwareTilt,
  WindowCovering.Feature.AbsolutePosition
);
var MovementType = /* @__PURE__ */ ((MovementType2) => {
  MovementType2[MovementType2["Lift"] = 0] = "Lift";
  MovementType2[MovementType2["Tilt"] = 1] = "Tilt";
  return MovementType2;
})(MovementType || {});
var CalibrationMode = /* @__PURE__ */ ((CalibrationMode2) => {
  CalibrationMode2[CalibrationMode2["Enabled"] = 0] = "Enabled";
  CalibrationMode2[CalibrationMode2["Running"] = 1] = "Running";
  CalibrationMode2[CalibrationMode2["Disabled"] = 2] = "Disabled";
  return CalibrationMode2;
})(CalibrationMode || {});
var MovementDirection = /* @__PURE__ */ ((MovementDirection2) => {
  MovementDirection2[MovementDirection2["Open"] = 0] = "Open";
  MovementDirection2[MovementDirection2["Close"] = 1] = "Close";
  MovementDirection2[MovementDirection2["DefinedByPosition"] = 2] = "DefinedByPosition";
  return MovementDirection2;
})(MovementDirection || {});
const WC_PERCENT100THS_MIN_OPEN = 0;
const WC_PERCENT100THS_MAX_CLOSED = 1e4;
const WC_PERCENT100THS_COEFFICIENT = 100;
class WindowCoveringServerLogic extends WindowCoveringServerBase {
  initialize() {
    this.internal.inMaintenanceMode = !!this.state.mode.maintenanceMode;
    this.internal.calibrationMode = this.state.mode.calibrationMode && !this.state.mode.maintenanceMode ? 0 /* Enabled */ : 2 /* Disabled */;
    this.reactTo(this.events.mode$Changing, this.#handleModeChanging);
    const configStatus = this.state.configStatus;
    configStatus.operational = !this.internal.inMaintenanceMode && !this.state.mode.calibrationMode;
    if (this.features.lift) {
      if (this.features.positionAwareLift) {
        configStatus.liftPositionAware = true;
      }
    }
    if (this.features.tilt) {
      if (this.features.positionAwareTilt) {
        configStatus.tiltPositionAware = true;
      }
    }
    if (!isDeepEqual(configStatus, this.state.configStatus)) {
      this.state.configStatus = configStatus;
    }
    if (this.features.tilt) {
      this.state.targetPositionTiltPercent100ths = this.state.currentPositionTiltPercent100ths;
    }
    if (this.features.lift) {
      this.state.targetPositionLiftPercent100ths = this.state.currentPositionLiftPercent100ths;
    }
    this.reactTo(this.events.currentPositionLiftPercent100ths$Changing, this.#syncLiftCurrentPositions);
    this.reactTo(this.events.currentPositionTiltPercent100ths$Changing, this.#syncTiltCurrentPositions);
    this.reactTo(this.events.targetPositionLiftPercent100ths$Changing, this.#handleLiftTargetPositionChanging);
    this.reactTo(this.events.targetPositionTiltPercent100ths$Changing, this.#handleTiltTargetPositionChanging);
    this.reactTo(this.events.operationalStatus$Changing, this.#handleOperationalStatusChanging);
  }
  /**
   * Sync the mode attribute with the configStatus attribute and the internal state.
   */
  #handleModeChanging(mode) {
    if (mode.maintenanceMode && mode.calibrationMode) {
      mode.calibrationMode = false;
    }
    if (mode.maintenanceMode && !this.state.supportsMaintenanceMode) {
      throw new StatusResponseError("Maintenance mode not supported", StatusCode.ConstraintError);
    }
    this.internal.inMaintenanceMode = !!mode.maintenanceMode;
    if (mode.calibrationMode) {
      if (!this.state.supportsCalibration) {
        throw new StatusResponseError("Calibration not supported", StatusCode.ConstraintError);
      }
      if (this.internal.calibrationMode === 1 /* Running */) {
      } else {
        this.internal.calibrationMode = 0 /* Enabled */;
      }
    } else {
      this.internal.calibrationMode = 2 /* Disabled */;
    }
    const configStatus = this.state.configStatus;
    configStatus.operational = !mode.maintenanceMode || mode.calibrationMode && !this.state.supportsCalibration;
    configStatus.liftMovementReversed = !!mode.motorDirectionReversed;
    if (isDeepEqual(configStatus, this.state.configStatus)) {
      this.asAdmin(() => {
        this.state.configStatus = configStatus;
      });
    }
    logger.debug(
      `Mode changed to ${Logger.toJSON(mode)} and config status to ${Logger.toJSON(configStatus)} and internal calibration mode to ${this.internal.calibrationMode}`
    );
  }
  /** Update the global operational status based on the lift or tilt status. */
  #handleOperationalStatusChanging(operationalStatus) {
    const globalStatus = operationalStatus.lift !== WindowCovering.MovementStatus.Stopped ? operationalStatus.lift : operationalStatus.tilt;
    operationalStatus.global = globalStatus;
    logger.debug(
      `Operational status changed to ${Logger.toJSON(operationalStatus)} with new global status ${globalStatus}`
    );
    this.state.operationalStatus = operationalStatus;
  }
  /** Update the operational state when the target lift position changes. */
  #handleLiftTargetPositionChanging(percent100ths) {
    if (this.features.positionAwareLift) {
      this.state.operationalStatus.lift = this.#computeOperationalState(
        percent100ths,
        this.state.currentPositionLiftPercent100ths
      );
    }
  }
  /** Update the operational state when the target tilt position changes. */
  #handleTiltTargetPositionChanging(percent100ths) {
    if (this.features.positionAwareTilt) {
      this.state.operationalStatus.tilt = this.#computeOperationalState(
        percent100ths,
        this.state.currentPositionTiltPercent100ths
      );
    }
  }
  /** Sync the current lift position attributes and the operational state. */
  #syncLiftCurrentPositions(percent100ths) {
    if (this.features.positionAwareLift) {
      this.state.currentPositionLiftPercentage = percent100ths === null ? percent100ths : Math.floor(percent100ths / WC_PERCENT100THS_COEFFICIENT);
      if (this.state.operationalStatus.lift !== WindowCovering.MovementStatus.Stopped && percent100ths === this.state.targetPositionLiftPercent100ths) {
        this.state.operationalStatus.lift = WindowCovering.MovementStatus.Stopped;
        logger.debug("Lift movement stopped, target value reached");
      }
    }
    if (this.features.absolutePosition) {
      this.state.currentPositionLift = percent100ths === null ? null : this.#percent100thsToLift(percent100ths);
    }
    logger.debug(
      `Syncing lift position ${this.state.currentPositionLiftPercent100ths === null ? null : (this.state.currentPositionLiftPercent100ths / 100).toFixed(2)} to ${this.state.currentPositionLiftPercentage}% (${this.state.currentPositionLift})`
    );
  }
  /** Sync the current tilt position attributes and the operational state. */
  #syncTiltCurrentPositions(percent100ths) {
    if (this.features.positionAwareTilt) {
      this.state.currentPositionTiltPercentage = percent100ths === null ? percent100ths : Math.floor(percent100ths / WC_PERCENT100THS_COEFFICIENT);
      if (this.state.operationalStatus.tilt !== WindowCovering.MovementStatus.Stopped && percent100ths === this.state.targetPositionTiltPercent100ths) {
        this.state.operationalStatus.tilt = WindowCovering.MovementStatus.Stopped;
        logger.debug("Tilt movement stopped, target value reached");
      }
    }
    if (this.features.absolutePosition) {
      this.state.currentPositionTilt = percent100ths === null ? null : this.#percent100thsToTilt(percent100ths);
    }
    logger.debug(
      `Syncing tilt position ${this.state.currentPositionTiltPercent100ths === null ? null : (this.state.currentPositionTiltPercent100ths / 100).toFixed(2)} to ${this.state.currentPositionTiltPercentage}% (${this.state.currentPositionTilt})`
    );
  }
  /** Compute the operational state based on the current and target position. */
  #computeOperationalState(target, current) {
    if (current === null || target === null) {
      return WindowCovering.MovementStatus.Stopped;
    } else if (current === target) {
      return WindowCovering.MovementStatus.Stopped;
    } else if (current < target) {
      return WindowCovering.MovementStatus.Closing;
    } else {
      return WindowCovering.MovementStatus.Opening;
    }
  }
  /**
   * Asserts if the device can be controlled or not because of an active Maintenance mode or a calibration is
   * required but not supported.
   */
  #assertMotionLockStatus() {
    if (this.internal.inMaintenanceMode) {
      throw new StatusResponseError("Device is in maintenance mode", StatusCode.Busy);
    }
    switch (this.internal.calibrationMode) {
      case 0 /* Enabled */:
        if (!this.state.supportsCalibration) {
          throw new StatusResponseError("Calibration not implemented", StatusCode.Failure);
        }
        break;
      case 1 /* Running */:
        break;
      case 2 /* Disabled */:
        break;
    }
    if (!this.state.configStatus.operational) {
      throw new StatusResponseError("Device is not operational", StatusCode.Failure);
    }
  }
  /**
   * Calibrate the device.  The default implementation takes no action. Override to implement calibration if
   * necessary.
   *
   * @protected
   */
  executeCalibration() {
  }
  /**
   * Perform actual "movement".  Override to initiate movement of your device.
   *
   * The default implementation logs and immediately updates current position to the target positions.  This is
   * probably not desirable for a real device so do not invoke `super.handleMovement()` from your implementation.
   *
   * @param type Which element should move, Lift or Tilt
   * @param reversed If the motor is configured reversed
   * @param direction The direction of the movement (Open, Close, DefinedByPosition)
   * @param targetPercent100ths Optionally the target position in percent 100ths. It depends on the used feature set
   * of the cluster if this is provided or not.
   *
   * @protected
   */
  async handleMovement(type, reversed, direction, targetPercent100ths) {
    switch (type) {
      case 0 /* Lift */:
        if (this.features.positionAwareLift) {
          if (targetPercent100ths === void 0) {
            throw new ImplementationError("Target position must be defined for position aware lift");
          }
          this.state.currentPositionLiftPercent100ths = targetPercent100ths;
        }
        break;
      case 1 /* Tilt */:
        if (this.features.positionAwareTilt) {
          if (targetPercent100ths === void 0) {
            throw new ImplementationError("Target position must be defined for position aware lift");
          }
          this.state.currentPositionTiltPercent100ths = targetPercent100ths;
        }
        break;
    }
    const directionInfo = direction === 2 /* DefinedByPosition */ ? ` in direction by position` : ` in direction ${direction === 1 /* Close */ ? "Close" : "Open"}`;
    const targetInfo = targetPercent100ths === void 0 ? "" : ` to target position ${(targetPercent100ths / 100).toFixed(2)}`;
    logger.debug(
      `Moving the device ${type === 0 /* Lift */ ? "Lift" : "Tilt"}${directionInfo} (reversed=${reversed})${targetInfo}`
    );
  }
  /**
   * Handle a movement. If calibration is supported and needed then {@link executeCalibration} runs before the actual
   * movement. The method increases the numberOfActuations* attribute and updates the operational status.
   *
   * Actual movement occurs in {@link handleMovement} as a worker. Thus this method returns before actual movement
   * completes.
   */
  #prepareMovement(type, direction, targetPercent100ths) {
    if (this.state.supportsCalibration && this.internal.calibrationMode === 0 /* Enabled */) {
      return this.env.runtime.add(this.#executeCalibrationAndMove(type, direction, targetPercent100ths));
    }
    if (type === 0 /* Lift */ && this.state.configStatus.liftMovementReversed) {
      logger.debug("Lift movement is reversed");
    }
    switch (type) {
      case 0 /* Lift */:
        this.state.numberOfActuationsLift = (this.state.numberOfActuationsLift ?? 0) + 1;
        if (this.features.positionAwareLift && direction === 2 /* DefinedByPosition */ && targetPercent100ths !== void 0 && this.state.currentPositionLiftPercent100ths !== null) {
          direction = targetPercent100ths > this.state.currentPositionLiftPercent100ths ? 1 /* Close */ : 0 /* Open */;
        }
        if (direction !== 2 /* DefinedByPosition */) {
          this.state.operationalStatus.lift = direction === 1 /* Close */ ? WindowCovering.MovementStatus.Closing : WindowCovering.MovementStatus.Opening;
        }
        break;
      case 1 /* Tilt */:
        this.state.numberOfActuationsTilt = (this.state.numberOfActuationsTilt ?? 0) + 1;
        if (this.features.positionAwareLift && direction === 2 /* DefinedByPosition */ && targetPercent100ths !== void 0 && this.state.currentPositionTiltPercent100ths !== null) {
          direction = targetPercent100ths > this.state.currentPositionTiltPercent100ths ? 1 /* Close */ : 0 /* Open */;
        }
        if (direction !== 2 /* DefinedByPosition */) {
          this.state.operationalStatus.tilt = direction === 1 /* Close */ ? WindowCovering.MovementStatus.Closing : WindowCovering.MovementStatus.Opening;
        }
        break;
    }
    this.env.runtime.add(
      this.handleMovement(
        type,
        type === 0 /* Lift */ && !!this.state.configStatus.liftMovementReversed,
        direction,
        targetPercent100ths
      )
    );
  }
  #executeCalibrationAndMove(type, direction, targetPercent100ths) {
    let calibration;
    if (this.internal.calibrationMode === 0 /* Enabled */ && this.state.supportsCalibration) {
      this.internal.calibrationMode = 1 /* Running */;
      calibration = this.executeCalibration();
    }
    return MaybePromise.then(calibration, () => {
      this.internal.calibrationMode = 2 /* Disabled */;
      return this.#prepareMovement(type, direction, targetPercent100ths);
    });
  }
  /**
   * Stop device movement.  Sets the target position to the current position and updates operational state. Override
   * to implement the actual stop movement logic.
   *
   * If you update the current positions you can include the default logic via "super.handleStopMovement()".
   *
   * @protected
   */
  handleStopMovement() {
    if (this.features.positionAwareLift) {
      this.state.targetPositionLiftPercent100ths = this.state.currentPositionLiftPercent100ths;
    }
    if (this.features.positionAwareTilt) {
      this.state.targetPositionTiltPercent100ths = this.state.currentPositionTiltPercent100ths;
    }
    if (!this.features.positionAwareLift && !this.features.positionAwareTilt) {
      this.state.operationalStatus = {
        global: WindowCovering.MovementStatus.Stopped,
        lift: WindowCovering.MovementStatus.Stopped,
        tilt: WindowCovering.MovementStatus.Stopped
      };
    }
  }
  #triggerLiftMotion(direction, targetPercent100ths) {
    this.#prepareMovement(0, direction, targetPercent100ths);
  }
  #triggerTiltMotion(direction, targetPercent100ths) {
    this.#prepareMovement(1, direction, targetPercent100ths);
  }
  /**
   * Move the WindowCovering up or open. For position aware devices the target position is set to 0%. The method calls
   * the handleMovement method to actually move the device.
   */
  upOrOpen() {
    this.#assertMotionLockStatus();
    let targetLiftPercent100ths;
    let targetTiltPercent100ths;
    if (this.features.positionAwareLift) {
      this.state.targetPositionLiftPercent100ths = targetLiftPercent100ths = WC_PERCENT100THS_MIN_OPEN;
    }
    if (this.features.positionAwareTilt) {
      this.state.targetPositionTiltPercent100ths = targetTiltPercent100ths = WC_PERCENT100THS_MIN_OPEN;
    }
    if (this.features.lift) {
      this.#triggerLiftMotion(0 /* Open */, targetLiftPercent100ths);
    }
    if (this.features.tilt) {
      this.#triggerTiltMotion(0 /* Open */, targetTiltPercent100ths);
    }
  }
  /**
   * Move the WindowCovering down or close. For position aware devices the target position is set to 100%. The method
   * calls the handleMovement method to actually move the device.
   */
  downOrClose() {
    this.#assertMotionLockStatus();
    let targetLiftPercent100ths;
    let targetTiltPercent100ths;
    if (this.features.positionAwareLift) {
      this.state.targetPositionLiftPercent100ths = targetLiftPercent100ths = WC_PERCENT100THS_MAX_CLOSED;
    }
    if (this.features.positionAwareTilt) {
      this.state.targetPositionTiltPercent100ths = targetTiltPercent100ths = WC_PERCENT100THS_MAX_CLOSED;
    }
    if (this.features.lift) {
      this.#triggerLiftMotion(1 /* Close */, targetLiftPercent100ths);
    }
    if (this.features.tilt) {
      this.#triggerTiltMotion(1 /* Close */, targetTiltPercent100ths);
    }
  }
  /**
   * Stop any movement of the WindowCovering. The method calls the handleStopMovement method to actually stop the
   * movement of the device.
   */
  stopMotion() {
    this.#assertMotionLockStatus();
    return this.handleStopMovement();
  }
  /**
   * Move the WindowCovering to a specific lift value. The default implementation calculates the % value for the
   * target position. The method calls the handleMovement method to actually move the device to the defined position.
   */
  goToLiftValue({ liftValue }) {
    this.#assertMotionLockStatus();
    this.state.targetPositionLiftPercent100ths = this.#liftToPercent100ths(liftValue);
    this.#triggerLiftMotion(2 /* DefinedByPosition */, this.state.targetPositionLiftPercent100ths);
  }
  /**
   * Move the WindowCovering to a specific tilt value. The method calls the handleMovement method to actually move the
   * device to the defined position.
   */
  goToLiftPercentage({ liftPercent100thsValue }) {
    this.#assertMotionLockStatus();
    if (this.features.positionAwareLift) {
      this.state.targetPositionLiftPercent100ths = liftPercent100thsValue;
      this.#triggerLiftMotion(
        2 /* DefinedByPosition */,
        this.state.targetPositionLiftPercent100ths ?? void 0
      );
    } else {
      if (liftPercent100thsValue === 0) {
        this.upOrOpen();
      } else {
        this.downOrClose();
      }
    }
  }
  /**
   * Move the WindowCovering to a specific tilt value. The default implementation calculates the % value for the target
   * position. The method calls the handleMovement method to actually move the device to the defined position.
   */
  goToTiltValue({ tiltValue }) {
    this.#assertMotionLockStatus();
    this.state.targetPositionTiltPercent100ths = this.#tiltToPercent100ths(tiltValue);
    this.#triggerTiltMotion(2 /* DefinedByPosition */, this.state.targetPositionTiltPercent100ths);
  }
  /**
   * Move the WindowCovering to a specific tilt value. The method calls the handleMovement method to actually move the
   * device to the defined position.
   */
  goToTiltPercentage({ tiltPercent100thsValue }) {
    this.#assertMotionLockStatus();
    if (this.features.positionAwareTilt) {
      this.state.targetPositionTiltPercent100ths = tiltPercent100thsValue ?? null;
      this.#triggerTiltMotion(
        2 /* DefinedByPosition */,
        this.state.targetPositionTiltPercent100ths ?? void 0
      );
    } else {
      if (tiltPercent100thsValue === 0) {
        this.upOrOpen();
      } else {
        this.downOrClose();
      }
    }
  }
  /**
   * ConvertValue: Converts values from one range to another
   * * Range In  -> from  inputLowValue to   inputHighValue
   * * Range Out -> from outputLowValue to outputHighValue
   */
  #convertValue(inputLowValue, inputHighValue, outputLowValue, outputHighValue, value) {
    let inputMin = inputLowValue;
    let inputMax = inputHighValue;
    let outputMin = outputLowValue;
    let outputMax = outputHighValue;
    if (inputLowValue > inputHighValue) {
      inputMin = inputHighValue;
      inputMax = inputLowValue;
    }
    if (outputLowValue > outputHighValue) {
      outputMin = outputHighValue;
      outputMax = outputLowValue;
    }
    const inputRange = inputMax - inputMin;
    const outputRange = outputMax - outputMin;
    if (value < inputMin) {
      return outputMin;
    }
    if (value > inputMax) {
      return outputMax;
    }
    if (inputRange > 0) {
      return Math.round(outputMin + outputRange * (value - inputMin) / inputRange);
    }
    return outputMax;
  }
  #valueToPercent100ths(limits, absolute) {
    return this.#convertValue(
      limits.open,
      limits.closed,
      WC_PERCENT100THS_MIN_OPEN,
      WC_PERCENT100THS_MAX_CLOSED,
      absolute
    );
  }
  #percent100thsToValue(limits, relative) {
    return this.#convertValue(
      WC_PERCENT100THS_MIN_OPEN,
      WC_PERCENT100THS_MAX_CLOSED,
      limits.open,
      limits.closed,
      relative
    );
  }
  #liftToPercent100ths(lift) {
    return this.#valueToPercent100ths(
      { open: this.state.installedOpenLimitLift, closed: this.state.installedClosedLimitLift },
      lift
    );
  }
  #percent100thsToLift(percent100ths) {
    return this.#percent100thsToValue(
      { open: this.state.installedOpenLimitLift, closed: this.state.installedClosedLimitLift },
      percent100ths
    );
  }
  #tiltToPercent100ths(tilt) {
    return this.#valueToPercent100ths(
      { open: this.state.installedOpenLimitTilt, closed: this.state.installedClosedLimitTilt },
      tilt
    );
  }
  #percent100thsToTilt(percent100ths) {
    return this.#percent100thsToValue(
      { open: this.state.installedOpenLimitTilt, closed: this.state.installedClosedLimitTilt },
      percent100ths
    );
  }
}
((WindowCoveringServerLogic2) => {
  class Internal {
    /** Status of the Device Calibration mode. */
    calibrationMode = 2 /* Disabled */;
    /** Status of the Device Maintenance mode. */
    inMaintenanceMode = false;
  }
  WindowCoveringServerLogic2.Internal = Internal;
  class State extends WindowCoveringServerBase.State {
    /** Does the device supports calibration? */
    supportsCalibration = false;
    /** Does the device supports maintenance mode? */
    supportsMaintenanceMode = true;
  }
  WindowCoveringServerLogic2.State = State;
})(WindowCoveringServerLogic || (WindowCoveringServerLogic = {}));
class WindowCoveringServer extends WindowCoveringServerLogic.for(ClusterType(WindowCovering.Base)) {
}
export {
  CalibrationMode,
  MovementDirection,
  MovementType,
  WindowCoveringServer,
  WindowCoveringServerLogic
};
//# sourceMappingURL=WindowCoveringServer.js.map
