/**
 * @license
 * Copyright 2022-2024 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { Behavior } from "#behavior/Behavior.js";
import { ValidatedElements } from "#behavior/cluster/ValidatedElements.js";
import { ActionTracer } from "#behavior/context/ActionTracer.js";
import { NodeActivity } from "#behavior/context/NodeActivity.js";
import { OfflineContext } from "#behavior/context/server/OfflineContext.js";
import { Transaction } from "#behavior/state/transaction/Transaction.js";
import {
  camelize,
  Construction,
  describeList,
  Diagnostic,
  ImplementationError,
  InternalError,
  Lifecycle,
  Logger,
  MaybePromise,
  ReadOnlyError,
  UninitializedDependencyError
} from "#general";
import { FeatureSet } from "#model";
import { DescriptorServer } from "../../behaviors/descriptor/DescriptorServer.js";
import { EndpointVariableService } from "../EndpointVariableService.js";
import { BehaviorInitializationError, EndpointBehaviorsError } from "../errors.js";
import { EndpointInitializer } from "./EndpointInitializer.js";
import { EndpointLifecycle } from "./EndpointLifecycle.js";
const logger = Logger.get("Behaviors");
class Behaviors {
  #endpoint;
  #supported;
  #backings = {};
  #options;
  /**
   * The {@link SupportedBehaviors} of the {@link Endpoint}.
   */
  get supported() {
    return this.#supported;
  }
  get status() {
    const status = {};
    for (const key in this.#supported) {
      status[key] = this.#backings[key]?.status ?? Lifecycle.Status.Inactive;
    }
    return status;
  }
  get [Diagnostic.value]() {
    return Diagnostic.lifecycleList(this.status);
  }
  get detailedDiagnostic() {
    return Object.entries(this.#supported).map(([name, type]) => {
      const backing = this.#backings[name];
      const result = [Diagnostic(backing?.status ?? Lifecycle.Status.Inactive, name)];
      const cluster = clusterOf(type);
      if (!cluster) {
        return result;
      }
      const elementDiagnostic = Array();
      const elements = new ValidatedElements(type);
      const features = new FeatureSet(cluster.supportedFeatures);
      if (features.size) {
        elementDiagnostic.push([Diagnostic.strong("features"), features]);
      }
      if (elements.attributes.size) {
        elementDiagnostic.push([Diagnostic.strong("attributes"), elements.attributes]);
      }
      if (elements.commands.size) {
        elementDiagnostic.push([Diagnostic.strong("commands"), elements.commands]);
      }
      if (elements.events.size) {
        elementDiagnostic.push([Diagnostic.strong("events"), elements.events]);
      }
      if (elementDiagnostic.length) {
        result.push(Diagnostic.list(elementDiagnostic));
      }
      return result;
    });
  }
  constructor(endpoint, options) {
    const { type } = endpoint;
    if (typeof type?.behaviors !== "object") {
      throw new ImplementationError('EndpointType "behaviors" must be an array of Behavior.Type instances');
    }
    this.#endpoint = endpoint;
    this.#supported = type.behaviors;
    this.#options = options;
    if (!this.#supported.descriptor) {
      this.#supported.descriptor = DescriptorServer;
    }
    for (const id in this.#supported) {
      const type2 = this.#supported[id];
      if (!(type2.prototype instanceof Behavior)) {
        throw new ImplementationError(`${endpoint}.${id}" is not a Behavior.Type`);
      }
      if (typeof type2.id !== "string") {
        throw new ImplementationError(`${endpoint}.${id} has no ID`);
      }
      this.#augmentEndpoint(type2);
    }
  }
  /**
   * Activate any behaviors designated for immediate activation.  Returns a promise iff any behaviors have ongoing
   * initialization.
   *
   * Throws an error if any behavior crashes, but we allow all behaviors to settle before throwing.  The goal is to
   * surface multiple configuration errors and prevent inconsistent state caused by partial initialization.
   */
  initialize() {
    if (!this.#endpoint.lifecycle.isInstalled) {
      throw new ImplementationError(`Cannot initialize behaviors because endpoint is not installed`);
    }
    const initializeBehaviors = (context) => {
      const agent = context.agentFor(this.#endpoint);
      for (const type of Object.values(this.supported)) {
        if (type.early) {
          this.activate(type, agent);
        }
      }
      return Construction.all(
        {
          [Symbol.iterator]: () => {
            return Object.values(this.#backings)[Symbol.iterator]();
          }
        },
        (causes) => new EndpointBehaviorsError(causes)
      );
    };
    const activity = this.#endpoint.env.get(NodeActivity);
    const trace = this.#endpoint.env.has(ActionTracer) ? { type: ActionTracer.ActionType.Initialize } : void 0;
    let promise = OfflineContext.act(`initialize<${this.#endpoint}>`, activity, initializeBehaviors, { trace });
    const onReady = () => {
      this.#endpoint.lifecycle.change(EndpointLifecycle.Change.Ready);
      if (trace) {
        trace.path = this.#endpoint.path;
        this.#endpoint.env.get(ActionTracer).record(trace);
      }
    };
    if (promise) {
      promise = promise.then(onReady);
    } else {
      onReady();
    }
    return promise;
  }
  /**
   * Does the {@link Endpoint} support a specified behavior?
   */
  has(type) {
    const myType = this.#supported[type.id];
    return myType === type || myType?.supports(type);
  }
  /**
   * Add behavior support dynamically at runtime.  Typically called via {@link Agent.require}.
   */
  require(type, options) {
    if (options) {
      this.#options[type.id] = options;
    }
    if (this.#supported[type.id]) {
      if (!this.has(type)) {
        throw new ImplementationError(
          `Cannot require ${this.#endpoint}.${type.id} because incompatible implementation already exists`
        );
      }
      return;
    }
    if (this.#supported === this.#endpoint.type.behaviors) {
      this.#supported = { ...this.#supported };
    }
    this.#supported[type.id] = type;
    this.#augmentEndpoint(type);
    this.#endpoint.lifecycle.change(EndpointLifecycle.Change.ServersChanged);
    if (type.early && this.#endpoint.lifecycle.isInstalled) {
      this.#activateLate(type);
    }
  }
  /**
   * Create a behavior synchronously.  Fails if the behavior is not fully initialized.
   */
  createSync(type, agent) {
    const behavior = this.createMaybeAsync(type, agent);
    if (MaybePromise.is(behavior)) {
      throw new ImplementationError(
        `Synchronous access to ${this.#endpoint}.${type.id} is impossible because it is still initializing`
      );
    }
    return behavior;
  }
  /**
   * Create a behavior asynchronously.  Waits for the behavior to complete initialization.
   */
  async createAsync(type, agent) {
    return MaybePromise.then(
      () => this.createMaybeAsync(type, agent),
      void 0,
      (e) => {
        const backing = this.#backings[type.id];
        if (!backing) {
          throw e;
        }
        backing.construction.assert(backing.toString());
      }
    );
  }
  /**
   * Create a behavior, possibly asynchronously.
   *
   * This method returns a {@link Promise} only if await is necessary so the behavior can be used immediately if
   * possible.
   */
  createMaybeAsync(type, agent) {
    this.activate(type, agent);
    const backing = this.#backings[type.id];
    const getBehavior = () => {
      if (agent.isLoaded(type)) {
        return agent.get(type);
      }
      return backing.createBehavior(agent, type);
    };
    if (backing.construction.status === Lifecycle.Status.Initializing) {
      return backing.construction.then(() => getBehavior()).catch(() => {
        backing.construction.assert(backing.toString());
        return getBehavior();
      });
    }
    backing.construction.assert(backing.toString());
    return getBehavior();
  }
  /**
   * Activate a behavior.
   *
   * Semantically identical to createAsync() but does not return a {@link Promise} or throw an error.
   *
   * Behaviors that fail initialization will be marked with crashed {@link status}.
   */
  activate(type, agent) {
    let backing = this.#backings[type.id];
    if (!backing) {
      backing = this.#createBacking(type, agent);
    }
  }
  /**
   * Determine if a specified behavior is supported and active.
   */
  isActive(type) {
    const backing = this.#backings[type.id];
    return !!backing && backing.type.supports(type);
  }
  /**
   * Destroy all behaviors that are initialized (have backings present).  The object may be reused after close.
   */
  async close() {
    const dispose = async (context) => {
      const agent = context.agentFor(this.#endpoint);
      let destroyNow = new Set(Object.keys(this.#backings));
      while (destroyNow.size) {
        for (const key in this.#backings) {
          const dependencies = this.#backings[key].type.dependencies;
          if (!dependencies) {
            continue;
          }
          for (const type of dependencies) {
            destroyNow.delete(type.id);
          }
          if (!destroyNow.size) {
            throw new ImplementationError("Cannot destroy behaviors due to circular dependency");
          }
        }
        for (const id of destroyNow) {
          await this.#backings[id].close(agent);
          delete this.#backings[id];
        }
        destroyNow = new Set(Object.keys(this.#backings));
      }
      const transaction = agent.context.transaction;
      if (transaction.status === Transaction.Status.Exclusive) {
        await transaction.commit();
      }
    };
    await OfflineContext.act(`close<${this.#endpoint}>`, this.#endpoint.env.get(NodeActivity), dispose);
  }
  /**
   * Ensure a set of behavior requirements are met.  Throws an error detailing missing requirements.
   */
  validateRequirements(requirements) {
    if (!requirements) {
      return;
    }
    const missing = Array();
    for (const requirement of Object.values(requirements)) {
      let name = camelize(requirement.name, true);
      if (this.#endpoint.behaviors.has(requirement)) {
        continue;
      }
      const cluster = clusterOf(requirement);
      if (cluster) {
        const other = this.#endpoint.behaviors.supported[requirement.id];
        if (clusterOf(other)?.id === cluster.id) {
          continue;
        }
        name = `${name} (0x${cluster.id.toString(16)})`;
      }
      missing.push(name);
    }
    if (missing.length) {
      throw new ImplementationError(
        `${this.#endpoint} is missing required behaviors: ${describeList("and", ...missing)}`
      );
    }
  }
  /**
   * Obtain default values for a behavior.  This is state values as present when the behavior is first initialized for
   * a new endpoint.
   */
  defaultsFor(type) {
    let defaults;
    const options = this.#options[type.id];
    if (options) {
      for (const key in type.defaults) {
        if (key in options) {
          if (!defaults) {
            defaults = {};
          }
          defaults[key] = options[key];
        }
      }
    }
    const varService = this.#endpoint.env.get(EndpointVariableService);
    const vars = varService.forBehaviorInstance(this.#endpoint, type);
    if (vars !== void 0) {
      defaults = { ...defaults, ...type.supervisor.cast(vars) };
    }
    return defaults;
  }
  /**
   * Access internal state for a {@link Behavior}.
   *
   * Internal state is not stable API and not intended for consumption outside of the behavior.  However it is not
   * truly private and may be accessed by tightly coupled implementation.
   *
   * As this API is intended for use by "friendly" code, it does not perform the same initialization assertions as
   * does access to {@link Behavior.State} and {@link Behavior.Events}.
   */
  internalsOf(type) {
    let backing = this.#backings[type.id];
    if (!backing) {
      this.#activateLate(type);
      backing = this.#backings[type.id];
      if (backing === void 0) {
        throw new InternalError(`Behavior ${this.#endpoint}.${type.id} late activation did not create backing`);
      }
    }
    return backing.getInternal();
  }
  #activateLate(type) {
    const result = OfflineContext.act(
      "behavior-late-activation",
      this.#endpoint.env.get(NodeActivity),
      (context) => this.activate(type, context.agentFor(this.#endpoint))
    );
    if (MaybePromise.is(result)) {
      result.then(void 0, (error) => {
        const backing = this.#backings[type.id];
        if (backing) {
          logger.error(`Error initializing ${backing}`, error);
        } else {
          logger.error(`Unexpected rejection initializing ${type.name}`, error);
        }
      });
    }
  }
  /**
   * Obtain a backing for an endpoint shortcut.
   */
  #backingFor(container, type) {
    if (this.#endpoint.construction.status !== Lifecycle.Status.Initializing) {
      this.#endpoint.construction.assert(this.#endpoint.toString(), `behavior ${type.id}`);
    }
    let backing = this.#backings[type.id];
    if (!backing) {
      try {
        this.#activateLate(type);
      } catch (e) {
        logger.warn(`Cannot initialize ${container}.${type.id} until node is initialized: ${e}`);
        throw new UninitializedDependencyError(
          `${container}.${type.id}`,
          "is not available until node is initialized, you may await node.construction to avoid this error"
        );
      }
      backing = this.#backings[type.id];
      if (backing === void 0) {
        throw new InternalError(`Behavior ${this.#endpoint}.${type.id} late activation did not create backing`);
      }
    }
    return backing;
  }
  #createBacking(type, agent) {
    const myType = this.#getBehaviorType(type);
    if (!myType) {
      throw new BehaviorInitializationError(`Initializing ${this.#endpoint}.${type.id}: Unsupported behavior`);
    }
    const backing = this.#endpoint.env.get(EndpointInitializer).createBacking(this.#endpoint, myType);
    this.#backings[type.id] = backing;
    backing.construction.start(agent);
    return backing;
  }
  #getBehaviorType(type) {
    const myType = this.#supported[type.id];
    if (myType === void 0) {
      return myType;
    }
    if (typeof myType !== "function" || !(myType.prototype instanceof Behavior)) {
      throw new ImplementationError(`Endpoint behavior "${type.id}" implementation is not a Behavior`);
    }
    return myType;
  }
  /**
   * Updates endpoint "state" and "events" properties to include properties for our implementations.
   */
  #augmentEndpoint(type) {
    Object.defineProperty(this.#endpoint.state, type.id, {
      get: () => {
        return this.#backingFor("state", type).stateView;
      },
      set() {
        throw new ReadOnlyError('The "state" property is read-only; you must use set() to modify state');
      },
      enumerable: true
    });
    let events;
    Object.defineProperty(this.#endpoint.events, type.id, {
      get: () => {
        if (!events) {
          events = new type.Events();
        }
        return events;
      },
      enumerable: true
    });
  }
}
function clusterOf(behavior) {
  return behavior?.cluster;
}
export {
  Behaviors
};
//# sourceMappingURL=Behaviors.js.map
