//const EventEmitter = require('events');

class movementManager {
  constructor(config, logger, emitter) {
    this.emitter = emitter; //new EventEmitter();  //state class emitter

    const { min, max, initial } = config.position;
    const { speed, maxSpeed, interval } = config.movement;

    this.minPosition = min;
    this.maxPosition = max;
    this.currentPosition = initial;

    this.speed = speed;
    this.maxSpeed = maxSpeed;
    this.interval = interval;
    this.timeleft = 0; // timeleft of current movement

    this.logger = logger;
    this.movementMode = config.movement.mode;
    this.logger?.debug?.(`MovementManager initialized: speed=${this.speed}, maxSpeed=${this.maxSpeed}`);
  }

  getCurrentPosition() {
    return this.currentPosition;
  }

  async moveTo(targetPosition, signal) {
    // Constrain target position if necessary
    if (
      targetPosition < this.minPosition ||
      targetPosition > this.maxPosition
    ) {
      targetPosition = this.constrain(targetPosition);
      this.logger.warn(
        `New target position=${targetPosition} is constrained to fit between min=${this.minPosition} and max=${this.maxPosition}`
      );
    }

    this.logger.info(
      `Starting movement to position ${targetPosition} in ${this.movementMode} with avg speed=${this.speed}%/s.`
    );

    if (signal && signal.aborted) {
      this.logger.debug("Movement aborted.");
      return;
    }

    try {
      // Execute the movement logic based on the mode
      switch (this.movementMode) {
        case "staticspeed": {
          const movelinFeedback = await this.moveLinear(targetPosition,signal);
          this.logger.info(`Linear move: ${movelinFeedback} `);
          break;
        }

        case "dynspeed": {
          const moveDynFeedback = await this.moveEaseInOut(targetPosition,signal);
          this.logger.info(`Dynamic move : ${moveDynFeedback}`);
          break;
        }

        default:
          throw new Error(`Unsupported movement mode: ${this.movementMode}`);
      }
    } catch (error) {
      this.logger.error(error);
    }
  }

  moveLinear(targetPosition, signal) {
  return new Promise((resolve, reject) => {
    // Immediate abort if already signalled
    if (signal?.aborted) {
      return reject(new Error("Movement aborted"));
    }

    // Clamp the final target into [minPosition, maxPosition]
    targetPosition = this.constrain(targetPosition);

    // Snapshot the starting point. Position is derived from ELAPSED WALL-TIME
    // (not accumulated per-tick steps) so an interruption that lands between
    // ticks — or before the very first tick — still leaves currentPosition at
    // the real distance travelled. A fast re-commanding parent (e.g. MGC
    // updating demand every tick) then re-bases from the true position instead
    // of freezing at the start. See _settleAt / the abort handler below.
    const startPosition = this.currentPosition;
    const direction = targetPosition > startPosition ? 1 : -1;
    const distance  = Math.abs(targetPosition - startPosition);

    const velocity = this.getVelocity(); // units per second
    if (velocity <= 0) {
      return reject(new Error("Movement aborted: zero speed"));
    }

    const duration  = distance / velocity;        // seconds to go the full distance
    this.timeleft   = duration;
    this.logger.debug(
      `Linear move: dir=${direction}, dist=${distance}, vel=${velocity.toFixed(2)} u/s, dur=${duration.toFixed(2)}s`
    );

    const intervalMs = this.interval;
    const startTime  = Date.now();

    // Position reached after `elapsedSec` of travel, clamped to the target.
    const posAt = (elapsedSec) =>
      this.constrain(startPosition + direction * Math.min(distance, velocity * elapsedSec));
    // Re-base currentPosition (and timeleft) onto the real elapsed progress.
    const settle = () => {
      const elapsed = (Date.now() - startTime) / 1000;
      this.currentPosition = posAt(elapsed);
      this.timeleft = Math.max(0, duration - elapsed);
      this.emitPos(this.currentPosition);
      return elapsed;
    };

    // Kick off the loop
    const intervalId = setInterval(() => {
      if (signal?.aborted) {
        clearInterval(intervalId);
        settle();
        return reject(new Error("Movement aborted"));
      }

      const elapsed = settle();
      this.logger.debug(
        `pos=${this.currentPosition.toFixed(2)}, timeleft=${this.timeleft.toFixed(2)}`
      );

      // Completed the move? (time-based so it can't overshoot/undershoot)
      if (elapsed >= duration) {
        clearInterval(intervalId);
        this.currentPosition = targetPosition;
        this.timeleft = 0;
        this.emitPos(this.currentPosition);
        return resolve("Reached target move.");
      }
    }, intervalMs);

    // Catch aborts that happen between ticks (incl. before the first tick):
    // capture the partial progress so the move re-bases instead of freezing.
    signal?.addEventListener("abort", () => {
      clearInterval(intervalId);
      settle();
      reject(new Error("Movement aborted"));
    });
  });
}

  moveLinearinTime(targetPosition,signal) {
    return new Promise((resolve, reject) => {
      // Abort immediately if already signalled
      if (signal?.aborted) {
        return reject(new Error("Movement aborted"));
      }
      
      const direction = targetPosition > this.currentPosition ? 1 : -1;
      const distance = Math.abs(targetPosition - this.currentPosition);

      const velocity = this.getVelocity();
      if (velocity <= 0) {
        return reject(new Error("Movement aborted: zero speed"));
      }
      const duration = distance / velocity;

      this.timeleft = duration; //set this so other classes can use it
      this.logger.debug(
        `Linear movement: Direction=${direction}, Distance=${distance}, Duration=${duration}s`
      );

      let elapsedTime = 0;
      const interval = this.interval; // Update every x ms
      const totalSteps = Math.ceil((duration * 1000) / interval);
      const stepSize = direction * (distance / totalSteps);

      // 2) Set up the abort listener once
      const intervalId = setInterval(() => {
        // 3) Check for abort on each tick
        if (signal?.aborted) {
          clearInterval(intervalId);
          return reject(new Error("Movement aborted"));
        }
        // Update elapsed time
        elapsedTime += interval / 1000;

        this.timeleft = duration - elapsedTime; //set this so other classes can use it

        // Update the position incrementally
        this.currentPosition += stepSize;
        this.emitPos(this.currentPosition);
        this.logger.debug(
          `Using ${this.movementMode} => Current position ${this.currentPosition}`
        );

        // Check if the target position has been reached
        if (
          (direction > 0 && this.currentPosition >= targetPosition) ||
          (direction < 0 && this.currentPosition <= targetPosition)
        ) {
          clearInterval(intervalId);
          this.currentPosition = targetPosition;
          resolve(`Reached target move.`);
        }
      }, interval);
      // Also attach abort outside the interval in case it fires before the first tick:
      signal?.addEventListener("abort", () => {
        clearInterval(intervalId);
        reject(new Error("Movement aborted"));
      });
    });
  }

  moveEaseInOut(targetPosition, signal) {
    return new Promise((resolve, reject) => {
      // 1) Bail immediately if already aborted
      if (signal?.aborted) {
        return reject(new Error("Movement aborted"));
      }

      const startPosition = this.currentPosition;
      const totalDistance = Math.abs(targetPosition - this.currentPosition);
      const velocity = this.getVelocity();
      if (velocity <= 0) {
        return reject(new Error("Movement aborted: zero speed"));
      }

      const easeFunction = (t) =>
        t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;

      const duration = totalDistance / velocity;
      this.timeleft = duration;
      const interval = this.interval;
      const startTime = Date.now();

      // Position from ELAPSED WALL-TIME (eased), so an interruption between
      // ticks re-bases from the real position rather than freezing at the
      // start — same rationale as moveLinear.
      const posAt = (elapsedSec) => {
        const progress = duration > 0 ? Math.min(elapsedSec / duration, 1) : 1;
        return startPosition + (targetPosition - startPosition) * easeFunction(progress);
      };
      const settle = () => {
        const elapsed = (Date.now() - startTime) / 1000;
        this.currentPosition = posAt(elapsed);
        this.timeleft = Math.max(0, duration - elapsed);
        this.emitPos(this.currentPosition);
        return elapsed;
      };

      // 2) Start the moving loop
      const intervalId = setInterval(() => {
        // 3) Check for abort on each tick
        if (signal?.aborted) {
          clearInterval(intervalId);
          settle();
          return reject(new Error("Movement aborted"));
        }

        const elapsed = settle();
        this.logger.debug(
          `Using ${this.movementMode} => elapsed=${elapsed.toFixed(2)}s, pos=${this.currentPosition.toFixed(2)}`
        );

        if (elapsed >= duration) {
          clearInterval(intervalId);
          this.currentPosition = targetPosition;
          this.timeleft = 0;
          this.emitPos(this.currentPosition);
          resolve(`Reached target move.`);
        }
      }, interval);

      // 4) Capture partial progress on aborts between/before ticks.
      signal?.addEventListener("abort", () => {
        clearInterval(intervalId);
        settle();
        reject(new Error("Movement aborted"));
      });
    });
  }

  emitPos(newPosition) {
    this.emitter.emit("positionChange", newPosition);
  }

  constrain(value) {
    return Math.min(Math.max(value, this.minPosition), this.maxPosition);
  }

  getNormalizedSpeed() {
    const rawSpeed = Number.isFinite(this.speed) ? this.speed : 0;
    const clampedSpeed = Math.max(0, rawSpeed);
    const hasMax = Number.isFinite(this.maxSpeed) && this.maxSpeed > 0;
    const effectiveSpeed = hasMax ? Math.min(clampedSpeed, this.maxSpeed) : clampedSpeed;
    return effectiveSpeed / 100; // convert %/s -> fraction of range per second
  }

  getVelocity() {
    const normalizedSpeed = this.getNormalizedSpeed();
    const fullRange = this.maxPosition - this.minPosition;
    return normalizedSpeed * fullRange;
  }
}

module.exports = movementManager;
