/**
 * @license
 * Copyright 2022-2026 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { asError, camelize, InternalError } from "@matter/general";
import { FieldValue, Metatype } from "../common/index.js";
import { Lexer, TokenStream } from "../parser/index.js";
import { Aspect } from "./Aspect.js";
class Conformance extends Aspect {
  ast;
  get type() {
    return this.ast.type;
  }
  /**
   * Initialize from a Conformance.Definition or the conformance DSL defined by the Matter Specification.
   */
  constructor(definition) {
    super(definition);
    if (definition === void 0) {
      this.ast = { type: Conformance.Special.Empty };
    } else {
      let ast;
      if (typeof definition === "string") {
        ast = ParsedAst(this, definition);
      } else if (Array.isArray(definition)) {
        const asts = definition.map((def) => ParsedAst(this, def));
        if (asts.length === 1) {
          ast = asts[0];
        } else {
          ast = {
            type: Conformance.Special.Otherwise,
            param: asts
          };
        }
      } else {
        ast = definition.ast;
      }
      this.ast = ast;
    }
    this.isEmpty = this.type === Conformance.Special.Empty;
    this.freeze();
  }
  extend(other) {
    return other.isEmpty ? this : other;
  }
  validateReferences(errorTarget, lookup) {
    return Conformance.validateReferences(this, this.ast, errorTarget, lookup);
  }
  validateComputation(errorTarget, features) {
    try {
      this.applicabilityFor({ definedFeatures: features ?? /* @__PURE__ */ new Set(), supportedFeatures: /* @__PURE__ */ new Set() });
    } catch (e) {
      errorTarget.error("CANNOT_COMPUTE_CONFORMANCE", `Error computing conformance: ${asError(e).message}`);
    }
  }
  /**
   * Is the associated element mandatory?
   *
   * This supports a limited subset of conformance and is only appropriate for field and requirement conformance.
   */
  get isMandatory() {
    const conformance = this.ast;
    if (conformance.type === Conformance.Flag.Mandatory) {
      return true;
    }
    if (conformance.type === Conformance.Special.Otherwise) {
      for (const c of conformance.param) {
        if (c.type === Conformance.Flag.Provisional) {
          return false;
        }
        if (c.type === Conformance.Flag.Mandatory) {
          return true;
        }
      }
    }
    return false;
  }
  /**
   * Perform limited conformance evaluation to determine whether this conformance is applicable given a feature
   * combination.
   *
   * This is useful for filtering elements at compile time.  For complete accuracy you then need to filter at runtime
   * once field values are known.
   */
  applicabilityFor({ definedFeatures: features, supportedFeatures }) {
    return computeApplicability(features, supportedFeatures, this);
  }
  toString() {
    return Conformance.serialize(this.ast);
  }
  freeze() {
    freezeAst(this.ast);
    super.freeze();
  }
}
((Conformance2) => {
  let Applicability;
  ((Applicability2) => {
    Applicability2[Applicability2["None"] = 0] = "None";
    Applicability2[Applicability2["Optional"] = 1] = "Optional";
    Applicability2[Applicability2["Conditional"] = 2] = "Conditional";
    Applicability2[Applicability2["Mandatory"] = 3] = "Mandatory";
  })(Applicability = Conformance2.Applicability || (Conformance2.Applicability = {}));
  let Special;
  ((Special2) => {
    Special2["Empty"] = "empty";
    Special2["Desc"] = "desc";
    Special2["Name"] = "name";
    Special2["Value"] = "value";
    Special2["Choice"] = "choice";
    Special2["Otherwise"] = "otherwise";
    Special2["OptionalIf"] = "optionalIf";
  })(Special = Conformance2.Special || (Conformance2.Special = {}));
  let Flag;
  ((Flag2) => {
    Flag2["Mandatory"] = "M";
    Flag2["Optional"] = "O";
    Flag2["Provisional"] = "P";
    Flag2["Deprecated"] = "D";
    Flag2["Disallowed"] = "X";
  })(Flag = Conformance2.Flag || (Conformance2.Flag = {}));
  let Operator;
  ((Operator2) => {
    Operator2["NOT"] = "!";
    Operator2["EQ"] = "==";
    Operator2["NE"] = "!=";
    Operator2["OR"] = "|";
    Operator2["XOR"] = "^";
    Operator2["AND"] = "&";
    Operator2["DOT"] = ".";
    Operator2["GT"] = ">";
    Operator2["LT"] = "<";
    Operator2["GTE"] = ">=";
    Operator2["LTE"] = "<=";
  })(Operator = Conformance2.Operator || (Conformance2.Operator = {}));
  Conformance2.M = "M" /* Mandatory */;
  Conformance2.O = "O" /* Optional */;
  Conformance2.P = "P" /* Provisional */;
  Conformance2.D = "D" /* Deprecated */;
  Conformance2.X = "X" /* Disallowed */;
  Conformance2.EQ = "==" /* EQ */;
  Conformance2.NE = "!=" /* NE */;
  Conformance2.OR = "|" /* OR */;
  Conformance2.XOR = "^" /* XOR */;
  Conformance2.AND = "&" /* AND */;
  Conformance2.DOT = "." /* DOT */;
  Conformance2.GT = ">" /* GT */;
  Conformance2.LT = "<" /* LT */;
  Conformance2.GTE = ">=" /* GTE */;
  Conformance2.LTE = "<=" /* LTE */;
  function serializeAtomic(ast, otherOperator) {
    const serialized = serialize(ast);
    if (otherOperator !== void 0 && isHigherPrecedence(otherOperator, ast.type)) {
      return `(${serialized})`;
    }
    return serialized;
  }
  function validateReferences(conformance, ast, errorTarget, resolver) {
    switch (ast.type) {
      case "|" /* OR */:
      case "^" /* XOR */:
      case "&" /* AND */:
        validateReferences(conformance, ast.param.lhs, errorTarget, resolver);
        validateReferences(conformance, ast.param.rhs, errorTarget, resolver);
        break;
      case "==" /* EQ */:
      case "!=" /* NE */:
      case ">" /* GT */:
      case "<" /* LT */:
      case ">=" /* GTE */:
      case "<=" /* LTE */:
        validateReferences(conformance, ast.param.lhs, errorTarget, resolver);
        let operatorResolver = resolver;
        if (ast.param.lhs.type === "name") {
          const referenced = resolver(ast.param.lhs.param);
          if (referenced?.effectiveMetatype === Metatype.enum) {
            const enumDef = referenced.definingModel ?? referenced;
            operatorResolver = (name) => {
              const enumValue = enumDef.member(name) ?? enumDef.member(camelize(name, true));
              if (enumValue) {
                return enumValue;
              }
              return resolver(name);
            };
          }
        }
        validateReferences(conformance, ast.param.rhs, errorTarget, operatorResolver);
        break;
      case "!" /* NOT */:
        validateReferences(conformance, ast.param, errorTarget, resolver);
        break;
      case "otherwise" /* Otherwise */:
        for (const a of ast.param) {
          validateReferences(conformance, a, errorTarget, resolver);
        }
        break;
      case "name" /* Name */:
        if (!resolver(ast.param)) {
          errorTarget.error(
            `UNRESOLVED_CONFORMANCE_${ast.type.toUpperCase()}`,
            `Conformance ${ast.type} reference "${ast.param}" does not resolve`
          );
        }
        break;
    }
  }
  Conformance2.validateReferences = validateReferences;
  function serialize(ast) {
    switch (ast.type) {
      case "|" /* OR */:
      case "^" /* XOR */:
      case "&" /* AND */:
      case "==" /* EQ */:
      case "!=" /* NE */:
      case ">" /* GT */:
      case "<" /* LT */:
      case ">=" /* GTE */:
      case "<=" /* LTE */:
        const lhs = serializeAtomic(ast.param.lhs, ast.type);
        const rhs = serializeAtomic(ast.param.rhs, ast.type);
        return `${lhs} ${ast.type} ${rhs}`;
      case "!" /* NOT */:
        return `!${serializeAtomic(ast.param)}`;
      case "empty" /* Empty */:
        return "";
      case "desc" /* Desc */:
        return "desc";
      case "choice" /* Choice */:
        let result = `${serializeAtomic(ast.param.expr)}.${ast.param.name}`;
        if (ast.param.num > 1) {
          result = `${result}${ast.param.num}`;
        }
        if (ast.param.orMore) {
          if (!ast.param.orLess) {
            result = `${result}+`;
          }
        } else if (ast.param.orLess) {
          result = `${result}-`;
        }
        return result;
      case "otherwise" /* Otherwise */:
        return ast.param.map((d) => serialize(d)).join(", ");
      case "optionalIf" /* OptionalIf */:
        return `[${serialize(ast.param)}]`;
      case "name" /* Name */:
      case "value" /* Value */:
        return FieldValue.serialize(ast.param);
      default:
        return ast.type;
    }
  }
  Conformance2.serialize = serialize;
  function isBinaryOperator(type) {
    return Parser.BinaryOperators.has(type);
  }
  Conformance2.isBinaryOperator = isBinaryOperator;
  function precedenceOf(operator) {
    const index = Parser.BinaryOperatorPrecedence.findIndex((ops) => ops.indexOf(operator) !== -1);
    return index === -1 ? void 0 : index;
  }
  Conformance2.precedenceOf = precedenceOf;
  function isHigherPrecedence(operator, other) {
    const precedence1 = precedenceOf(operator);
    if (precedence1 === void 0) {
      return false;
    }
    const precedence2 = precedenceOf(other);
    if (precedence2 === void 0) {
      return false;
    }
    return precedence1 < precedence2;
  }
  Conformance2.isHigherPrecedence = isHigherPrecedence;
})(Conformance || (Conformance = {}));
const flags = new Set(Object.values(Conformance.Flag));
function ParsedAst(conformance, definition) {
  definition = definition.replace(" or ", " | ");
  const tokens = TokenStream(Lexer.Basic.lex(definition, (code, message) => conformance.error(code, message)));
  const otherwise = [];
  while (!tokens.done) {
    const optional = atOperator("[");
    if (optional) {
      tokens.next();
      let expr = {
        type: "optionalIf" /* OptionalIf */,
        param: parseExpression()
      };
      if (atOperator("]")) {
        tokens.next();
      } else {
        conformance.error("UNTERMINATED_CONFORMANCE_OPTIONAL", "Unterminated optional conformance group");
      }
      expr = parseChoice(expr);
      otherwise.push(expr);
    } else {
      const expr = parseExpression();
      if (expr) {
        otherwise.push(expr);
      }
    }
    if (atOperator(",")) {
      tokens.next();
    }
  }
  if (otherwise.length === 1) {
    return otherwise[0];
  }
  return {
    type: "otherwise" /* Otherwise */,
    param: otherwise
  };
  function atOperator(operator) {
    const { token } = tokens;
    return token && token.type === operator;
  }
  function parseExpression() {
    const elements = [];
    let expr = parseAtomicExpression();
    if (expr) {
      elements.push(expr);
    }
    while (tokens.token && Parser.BinaryOperators.has(tokens.token.type)) {
      elements.push(tokens.token.type);
      tokens.next();
      expr = parseAtomicExpression();
      if (expr) {
        elements.push(expr);
      }
    }
    Parser.BinaryOperatorPrecedence.forEach((operators2) => {
      for (let i = 0; i < elements.length; i++) {
        if (operators2.indexOf(elements[i + 1]) !== -1) {
          const [lhs, op, rhs] = elements.splice(i, 3);
          elements.splice(i, 0, {
            type: op,
            param: { lhs, rhs }
          });
          i--;
        }
      }
    });
    return elements[0];
  }
  function extractChoiceNameAndNumber(text) {
    let name = text[0];
    let num;
    if (name[0] < "a" || name[0] > "z") {
      conformance.error("INVALID_CHOICE", "Choice indicator is not a lowercase letter");
      name = "?";
      num = 1;
    } else if (text.length > 1) {
      num = Number.parseInt(text[1]);
      if (!Number.isFinite(num) || num < 0 || num > 9) {
        conformance.error("INVALID_CHOICE", "Choice indicator may only be a single lowercase letter");
        name = "?";
        num = 1;
      } else if (name.length > 2) {
        conformance.error("INVALID_CHOICE", "Choice number followed by unexpected word characters");
        name = "?";
      }
    } else {
      num = 1;
    }
    return { name, num };
  }
  function parseChoice(expr) {
    if (!atOperator(".")) {
      return expr;
    }
    tokens.next();
    let name, num;
    if (tokens.token?.type !== "word") {
      conformance.error("INVALID_CHOICE", 'Choice indicator (".") not followed by identifier');
      name = "?";
      num = 1;
    } else if (tokens.token.value.length > 2 && tokens.token.value[0] >= "A" && tokens.token.value[0] <= "Z" && expr.type === "name" /* Name */) {
      const fieldRef = `${expr.param}.${tokens.token.value}`;
      tokens.next();
      return { type: "name" /* Name */, param: fieldRef };
    } else {
      ({ name, num } = extractChoiceNameAndNumber(tokens.token.value));
      tokens.next();
    }
    const choice = { name, expr, num };
    if (atOperator("+")) {
      choice.orMore = true;
      tokens.next();
    }
    if (atOperator("-")) {
      choice.orLess = true;
      tokens.next();
    }
    return {
      type: "choice" /* Choice */,
      param: choice
    };
  }
  function parseAtomicExpression() {
    const expr = parseAtomicExpressionWithoutChoice();
    if (!expr) {
      return;
    }
    return parseChoice(expr);
  }
  function parseAtomicExpressionWithoutChoice() {
    if (!tokens.token) {
      conformance.error("PREMATURE_CONFORMANCE_TERMINATION", "Terminated with expression expected");
      return;
    }
    if (tokens.token.type === "word") {
      const name = tokens.token.value;
      tokens.next();
      if (flags.has(name)) {
        return { type: name };
      }
      switch (name.toLowerCase()) {
        case "desc":
          return { type: "desc" /* Desc */ };
        case "null":
          return { type: "value" /* Value */, param: null };
        case "true":
          return { type: "value" /* Value */, param: true };
        case "false":
          return { type: "value" /* Value */, param: false };
      }
      return { type: "name" /* Name */, param: name };
    }
    if (tokens.token.type === "value") {
      const value = tokens.token.value;
      tokens.next();
      return { type: "value" /* Value */, param: value };
    }
    if (atOperator("!")) {
      tokens.next();
      return { type: "!" /* NOT */, param: parseAtomicExpression() };
    }
    if (atOperator("(")) {
      tokens.next();
      const expr = parseExpression();
      if (atOperator(")")) {
        tokens.next();
      } else {
        conformance.error("UNTERMINATED_PARENTHETICAL_GROUP", `Unterminated parenthetical group`);
      }
      return expr;
    }
    conformance.error("UNEXPECTED_CONFORMANCE_TOKEN", `Unexpected "${tokens.token.type}"`);
    tokens.next();
  }
}
var Parser;
((Parser2) => {
  Parser2.BinaryOperatorPrecedence = [[">", "<", ">=", "<="], ["==", "!="], ["&"], ["|", "^"]];
  Parser2.BinaryOperators = new Set(Parser2.BinaryOperatorPrecedence.flat());
})(Parser || (Parser = {}));
const operators = new Set(Object.values(Conformance.Operator));
function computeApplicability(features, supportedFeatures, conformance) {
  const { None, Optional, Conditional, Mandatory } = Conformance.Applicability;
  const { ast } = conformance;
  if (ast.type === "otherwise" /* Otherwise */) {
    let fallback = None;
    for (const node of ast.param) {
      switch (assessOuterExpression(node)) {
        case Conditional:
          fallback = Conditional;
          break;
        case Optional:
          if (fallback !== Conditional) {
            fallback = Optional;
          }
          break;
        case Mandatory:
          return Mandatory;
      }
    }
    return fallback;
  }
  return assessOuterExpression(conformance.ast);
  function assessOuterExpression(ast2) {
    switch (ast2.type) {
      case "choice" /* Choice */:
        return assessOuterExpression(ast2.param.expr);
      case "O" /* Optional */:
      case "empty" /* Empty */:
        return Optional;
      case "optionalIf" /* OptionalIf */:
        const applicability = assessInnerExpression(ast2.param);
        if (applicability === Mandatory) {
          return Optional;
        }
        return applicability;
      case "X" /* Disallowed */:
      case "D" /* Deprecated */:
      case "P" /* Provisional */:
        return None;
      case "M" /* Mandatory */:
        return Mandatory;
      case "desc" /* Desc */:
        return Conditional;
    }
    return assessInnerExpression(ast2);
  }
  function assessInnerExpression(ast2) {
    switch (ast2.type) {
      case "name" /* Name */:
        if (features.has(ast2.param)) {
          return supportedFeatures.has(ast2.param) ? Mandatory : None;
        }
        return Conditional;
      case "!" /* NOT */:
        switch (assessInnerExpression(ast2.param)) {
          case None:
            return Mandatory;
          case Conditional:
            return Conditional;
          case Mandatory:
            return None;
        }
        break;
      case "&" /* AND */: {
        const lhs = assessInnerExpression(ast2.param.lhs);
        const rhs = assessInnerExpression(ast2.param.rhs);
        if (lhs === None || rhs === None) {
          return None;
        }
        if (lhs === Conditional || rhs === Conditional) {
          return Conditional;
        }
        return Mandatory;
      }
      case "|" /* OR */: {
        const lhs = assessInnerExpression(ast2.param.lhs);
        const rhs = assessInnerExpression(ast2.param.rhs);
        if (lhs === None && rhs === None) {
          return None;
        }
        if (lhs === Mandatory || rhs === Mandatory) {
          return Mandatory;
        }
        return Conditional;
      }
      default:
        if (operators.has(ast2.type)) {
          return Optional;
        }
    }
    throw new InternalError(`Conformance ${conformance}: Invalid node type ${ast2.type} in inner expression`);
  }
}
function freezeAst(ast) {
  switch (ast.type) {
    case "|" /* OR */:
    case "^" /* XOR */:
    case "&" /* AND */:
    case "==" /* EQ */:
    case "!=" /* NE */:
    case ">" /* GT */:
    case "<" /* LT */:
    case ">=" /* GTE */:
    case "<=" /* LTE */:
      freezeAst(ast.param.lhs);
      freezeAst(ast.param.rhs);
      Object.freeze(ast.param);
      break;
    case "!" /* NOT */:
    case "optionalIf" /* OptionalIf */:
      freezeAst(ast.param);
      break;
    case "value" /* Value */:
      if (typeof ast.param === "object" && ast.param !== null) {
        Object.freeze(ast.param);
      }
      break;
    case "choice" /* Choice */:
      freezeAst(ast.param.expr);
      Object.freeze(ast.param);
      break;
    case "otherwise" /* Otherwise */:
      for (const entry of ast.param) {
        freezeAst(entry);
      }
      Object.freeze(ast.param);
      break;
  }
  Object.freeze(ast);
}
export {
  Conformance
};
//# sourceMappingURL=Conformance.js.map
