/**
 * @license
 * Copyright 2022-2024 Matter.js Authors
 * SPDX-License-Identifier: Apache-2.0
 */
import { camelize } from "#general";
import { AttributeModel, ClusterModel, DataModelPath, FeatureMap, Metatype } from "#model";
import { StatusCode } from "#types";
import { ConformanceError, DatatypeError, SchemaImplementationError } from "../../errors.js";
import { Val } from "../Val.js";
import { Internal } from "../managed/Internal.js";
import {
  assertArray,
  assertBoolean,
  assertBytes,
  assertNumber,
  assertNumeric,
  assertObject,
  assertString
} from "./assertions.js";
import { createConformanceValidator } from "./conformance.js";
import { createConstraintValidator } from "./constraint.js";
function ValueValidator(schema, factory) {
  if (schema instanceof ClusterModel) {
    return createStructValidator(schema, factory) ?? (() => {
    });
  }
  let validator;
  const metatype = schema.effectiveMetatype;
  switch (metatype) {
    case Metatype.enum:
      validator = createEnumValidator(schema);
      break;
    case Metatype.bitmap:
      validator = createBitmapValidator(schema);
      break;
    case Metatype.integer:
    case Metatype.float:
      validator = createSimpleValidator(schema, assertNumeric);
      break;
    case Metatype.boolean:
      validator = createSimpleValidator(schema, assertBoolean);
      break;
    case Metatype.string:
      validator = createSimpleValidator(schema, assertString);
      break;
    case Metatype.bytes:
      validator = createSimpleValidator(schema, assertBytes);
      break;
    case Metatype.object:
      validator = createStructValidator(schema, factory);
      break;
    case Metatype.array:
      validator = createListValidator(schema, factory);
      break;
    case Metatype.date:
    case Metatype.any:
      break;
    case void 0:
      const type = schema.effectiveType;
      if (type === void 0) {
        if (schema.isDisallowed) {
          break;
        }
        throw new SchemaImplementationError(DataModelPath(schema.path), `No type defined`);
      }
      throw new SchemaImplementationError(
        DataModelPath(schema.path),
        `Cannot determine metatype for type "${type}"`
      );
    default:
      throw new SchemaImplementationError(
        DataModelPath(schema.path),
        `Unsupported validation metatype ${metatype}`
      );
  }
  validator = createNullValidator(schema, validator);
  validator = createConformanceValidator(schema, factory.featureMap, factory.supportedFeatures, validator);
  return validator;
}
function createNullValidator(schema, nextValidator) {
  if (schema.effectiveQuality.nullable === true) {
    return (value, options, location) => {
      if (value !== null) {
        nextValidator?.(value, options, location);
      }
    };
  }
  return nextValidator;
}
function createEnumValidator(schema) {
  const valid = new Set(schema.activeMembers.map((member) => member.id).filter((e) => e !== void 0));
  const constraint = schema.effectiveConstraint;
  const constraintValidator = constraint.in ? createConstraintValidator(schema.effectiveConstraint, schema) : void 0;
  return (value, session, location) => {
    assertNumber(value, location);
    if (!valid.has(value)) {
      throw new DatatypeError(location, "defined in enum", value, StatusCode.ConstraintError);
    }
    constraintValidator?.(value, session, location);
  };
}
function createBitmapValidator(schema) {
  const fields = {};
  for (const field of schema.activeMembers) {
    const constraint = field.effectiveConstraint;
    let max;
    if (typeof constraint.min === "number" && typeof constraint.max === "number") {
      max = constraint.max - constraint.min;
    } else {
      max = 1;
    }
    let name;
    if (field?.parent?.id === FeatureMap.id) {
      name = camelize(field.description ?? field.name);
    } else {
      name = camelize(field.name);
    }
    fields[name] = {
      schema: field,
      max
    };
  }
  return (value, _session, location) => {
    assertObject(value, location);
    for (const key in value) {
      const field = fields[key];
      const subpath = location.path.at(key);
      if (field === void 0) {
        throw new DatatypeError(subpath, "defined in bitmap", key);
      }
      const fieldValue = value[key];
      if (fieldValue === void 0) {
        continue;
      }
      if (field.max === 1) {
        assertBoolean(fieldValue, subpath);
      } else {
        assertNumber(fieldValue, subpath);
        if (fieldValue > field.max) {
          throw new DatatypeError(subpath, "in range of bit field", fieldValue);
        }
      }
    }
  };
}
function createSimpleValidator(schema, validateType) {
  const validateConstraint = createConstraintValidator(schema.effectiveConstraint, schema);
  return (value, session, location) => {
    if (value === void 0) {
      return;
    }
    validateType(value, location);
    validateConstraint?.(value, session, location);
  };
}
function createStructValidator(schema, factory) {
  const validators = {};
  for (const field of schema.activeMembers) {
    if (field.isDeprecated || AttributeModel.isGlobal(field)) {
      continue;
    }
    const validate = factory.get(field).validate;
    if (validate) {
      validators[camelize(field.name)] = validate;
    }
  }
  const validateStruct = (struct, session, location) => {
    assertObject(struct, location);
    const sublocation = {
      path: location.path.at(""),
      siblings: struct,
      choices: {}
    };
    for (const name in validators) {
      let value;
      if (struct[Val.properties]) {
        const rootOwner = struct[Internal.reference];
        const properties = struct[Val.properties](rootOwner, session);
        if (name in properties) {
          value = properties[name];
        } else {
          value = struct[name];
        }
      } else {
        value = struct[name];
      }
      sublocation.path.id = name;
      validators[name](value, session, sublocation);
    }
    for (const name in sublocation.choices) {
      const choice = sublocation.choices[name];
      if (choice.count < choice.target) {
        throw new ConformanceError(
          schema,
          location,
          `Too few fields present (${choice.count} of min ${choice.target})`,
          name
        );
      }
      if (choice.count > choice.target && !choice.orMore) {
        throw new ConformanceError(
          schema,
          location,
          `Too many fields present (${choice.count} of max ${choice.target})`,
          name
        );
      }
    }
  };
  return validateStruct;
}
function createListValidator(schema, factory) {
  const entry = schema.listEntry;
  let validateEntries;
  if (entry) {
    const entryValidator = factory.get(entry).validate;
    if (entryValidator) {
      validateEntries = (list, session, location) => {
        if (!list || typeof list[Symbol.iterator] !== "function") {
          throw new DatatypeError(location, "a list", list);
        }
        let index = 0;
        const sublocation = {
          path: location.path.at("")
        };
        for (const e of list) {
          if (e === void 0 || e === null) {
            continue;
          }
          sublocation.path.id = index;
          entryValidator(e, session, sublocation);
          index++;
        }
      };
    }
  }
  const validateConstraint = createConstraintValidator(schema.constraint, schema);
  return (value, session, location) => {
    assertArray(value, location);
    validateConstraint?.(value, session, location);
    validateEntries?.(value, session, location);
  };
}
export {
  ValueValidator
};
//# sourceMappingURL=ValueValidator.js.map
