/* * Trainlog database interface. * * Declares the module boundary and ownership contract; implementation and persistence remain in * their owning modules. */ #ifndef TRAINLOG_DATABASE_H #define TRAINLOG_DATABASE_H /** * @file database.h * @brief SQLite persistence API for the Trainlog TUI core. */ #include #include "trainlog/model.h" #include "trainlog/status.h" #define TRAINLOG_DATABASE_SCHEMA_VERSION 29 typedef struct TrainlogDatabase TrainlogDatabase; #define TRAINLOG_FEEDBACK_TEXT_MAX 8192U #define TRAINLOG_FEEDBACK_VIEW_MAX 4096U typedef struct TrainlogFeedbackView { char stable_id[TRAINLOG_ID_MAX + 1U]; char entry_id[TRAINLOG_ID_MAX + 1U]; /* empty for session follow-up */ char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U]; char raw_text[TRAINLOG_FEEDBACK_TEXT_MAX + 1U]; } TrainlogFeedbackView; /* CONTRACT: read-only chronological consultation; Android remains the sole * V1 creator. Ties use the stable ID byte order. No SQLite resources escape. */ TrainlogStatus trainlog_database_list_training_feedback(TrainlogDatabase *database, const char *session_id, TrainlogFeedbackView *exercise_feedback, size_t exercise_capacity, size_t *exercise_count, TrainlogFeedbackView *followups, size_t followup_capacity, size_t *followup_count); /* Nestable read savepoints let composition services observe one database state * without exposing SQLite or issuing writes to application data. */ TrainlogStatus trainlog_database_read_snapshot_begin(TrainlogDatabase *database); TrainlogStatus trainlog_database_read_snapshot_end(TrainlogDatabase *database, bool commit_snapshot); /* WHY: session occurrences retain an equipment ID, while custom definitions * need durable presentation metadata. A reference alone is never a definition. */ typedef struct TrainlogCustomEquipment { char equipment_id[TRAINLOG_ID_MAX + 1U]; char display_name[TRAINLOG_NAME_MAX + 1U]; char label_name[TRAINLOG_NAME_MAX + 1U]; char equipment_type[TRAINLOG_NAME_MAX + 1U]; char load_semantics[32]; } TrainlogCustomEquipment; #define TRAINLOG_CUSTOM_EQUIPMENT_PAGE_MAX 128U typedef enum TrainlogEquipmentOrigin { TRAINLOG_EQUIPMENT_SUPPLIED = 0, TRAINLOG_EQUIPMENT_CUSTOM, TRAINLOG_EQUIPMENT_UNKNOWN } TrainlogEquipmentOrigin; typedef struct TrainlogResolvedEquipment { char equipment_id[TRAINLOG_ID_MAX + 1U]; char display_name[TRAINLOG_NAME_MAX + 1U]; char label_name[TRAINLOG_NAME_MAX + 1U]; char equipment_type[TRAINLOG_NAME_MAX + 1U]; char load_semantics[32]; TrainlogEquipmentOrigin origin; } TrainlogResolvedEquipment; TrainlogStatus trainlog_database_create_custom_equipment(TrainlogDatabase *database, const TrainlogCustomEquipment *equipment); TrainlogStatus trainlog_database_list_custom_equipment(TrainlogDatabase *database, TrainlogCustomEquipment *output, size_t capacity, size_t *output_count); /* CONTRACT: reads at most capacity + 1 ordered definitions, so callers can * filter pages before imposing a presentation cap. database and output are * borrowed for the call; the caller owns copied strings. This read-only API * retains no resources and writes no persistence. capacity is 1..128 and all * pointer arguments are required. output_count is at most capacity; output_more * is true exactly when one additional ordered row exists. CONTRACT: output, * output_count, and output_more are valid only when this returns * TRAINLOG_STATUS_OK; on every other status callers must disregard all output, * including any storage copied before the failure. offset is valid only while * data is unchanged; refresh after writes. It is neither a durable cursor nor * an idempotency mechanism. */ TrainlogStatus trainlog_database_list_custom_equipment_page(TrainlogDatabase *database, size_t offset, TrainlogCustomEquipment *output, size_t capacity, size_t *output_count, bool *output_more); /* CONTRACT: an occurrence ID always resolves to a visible value. Unknown IDs * are returned verbatim with TRAINLOG_EQUIPMENT_UNKNOWN, never hidden. */ TrainlogStatus trainlog_database_resolve_equipment(TrainlogDatabase *database, const char *equipment_id, TrainlogResolvedEquipment *output); TrainlogStatus trainlog_database_list_exercise_equipment(TrainlogDatabase *database, const char *exercise_id, TrainlogResolvedEquipment *output, size_t capacity, size_t *output_count); TrainlogStatus trainlog_database_open(const char *path, TrainlogDatabase **output_database); /** * @brief Open a database and report the backend failure that prevented it. * * CONTRACT: @p output_diagnostic is optional. When supplied with a non-zero * capacity, it receives a NUL-terminated explanation of the failed SQLite * operation; callers still use the returned TrainlogStatus for control flow. * This preserves the stable application status surface without hiding the * path-specific reason needed to repair a user's durable database safely. */ TrainlogStatus trainlog_database_open_with_diagnostic(const char *path, TrainlogDatabase **output_database, char *output_diagnostic, size_t output_diagnostic_capacity); void trainlog_database_close(TrainlogDatabase *database); TrainlogStatus trainlog_database_schema_version(TrainlogDatabase *database, int *output_version); TrainlogStatus trainlog_database_foreign_keys_enabled(TrainlogDatabase *database, int *output_enabled); TrainlogStatus trainlog_database_begin(TrainlogDatabase *database); TrainlogStatus trainlog_database_commit(TrainlogDatabase *database); TrainlogStatus trainlog_database_rollback(TrainlogDatabase *database); /** * @brief Insert one already-normalized canonical exercise row. * * Unicode normalization belongs to catalog.c. This lower-level API owns the * final SQLite uniqueness barrier. */ TrainlogStatus trainlog_database_insert_exercise(TrainlogDatabase *database, const char *exercise_id, const char *name, const char *normalized_name, TrainlogTrackingMode tracking_mode); TrainlogStatus trainlog_database_insert_exercise_profiled(TrainlogDatabase *database, const char *exercise_id, const char *name, const char *normalized_name, TrainlogTrackingMode tracking_mode, TrainlogRecordingMode recording_mode, TrainlogExerciseDataFields data_fields); TrainlogStatus trainlog_database_exercise_count(TrainlogDatabase *database, size_t *output_count); TrainlogStatus trainlog_database_list_exercises(TrainlogDatabase *database, TrainlogExercise *output, size_t capacity, size_t *output_count); /** * @brief Resolve a current or merged exercise identity to its canonical ID. * * CONTRACT: current catalogue IDs resolve to themselves; durable legacy IDs * resolve through exercise_aliases. Unknown IDs return NOT_FOUND. The output * buffer is caller-owned and must hold TRAINLOG_ID_MAX + 1 bytes. */ TrainlogStatus trainlog_database_resolve_exercise_id(TrainlogDatabase *database, const char *exercise_id, char *output_canonical_id, size_t output_capacity); /** * @brief Atomically merge one current catalogue exercise into another. * * CONTRACT: source and canonical must be distinct current catalogue IDs. * Tracking/recording/data-field or conflicting non-empty primary-zone * profiles reject with CONFLICT and leave the database untouched. Compatible * direct zones are unioned; every occurrence is repointed without rewriting * its stable entry ID or any owned actual/planning data. Existing aliases to * source collapse directly to canonical and source becomes a durable alias. */ TrainlogStatus trainlog_database_merge_exercises(TrainlogDatabase *database, const char *source_exercise_id, const char *canonical_exercise_id); typedef struct TrainlogExerciseMergePreview { size_t occurrences; size_t performed_sets; size_t continuous_activities; size_t max_results; size_t associated_equipment; size_t body_zones; } TrainlogExerciseMergePreview; /* CONTRACT: counts describe source-owned records which the atomic merge will * repoint or union. Unknown IDs return NOT_FOUND and output is never partial. */ TrainlogStatus trainlog_database_preview_exercise_merge(TrainlogDatabase *database, const char *source_exercise_id, TrainlogExerciseMergePreview *output); /** * @brief Atomically replace one exercise's direct body-zone relations. * * @param primary_zone_id NULL or empty preserves the explicitly allowed * unclassified state, which requires secondary_count == 0. Group zones * cannot be assigned because their descendant relationship is derived * from the canonical manifest. * @param secondary_zone_ids Caller-owned array borrowed for this call only. * @param secondary_count Bounded by the number of canonical manifest zones. * @return INVALID_ARGUMENT for unknown/group/duplicate zones, NOT_FOUND for * an unknown exercise, or the explicit persistence status. */ TrainlogStatus trainlog_database_replace_exercise_body_zones(TrainlogDatabase *database, const char *exercise_id, const char *primary_zone_id, const char *const *secondary_zone_ids, size_t secondary_count); /* NOT_FOUND distinguishes an unknown exercise identity from the valid empty * relation set. Output is caller-owned; output_count reports required capacity * and no relation is truncated as a successful result. */ TrainlogStatus trainlog_database_list_exercise_body_zones(TrainlogDatabase *database, const char *exercise_id, TrainlogExerciseBodyZone *output, size_t capacity, size_t *output_count); /* CONTRACT: the caller owns output storage; output_count reports the required * count and INVALID_ARGUMENT is returned when capacity is insufficient. * Filtering combines a normalized name prefix with either one * direct zone, its manifest descendants, or the explicit unclassified state. * `primary_only` excludes secondary participation without changing storage. * zone_id and unclassified_only are mutually exclusive. * The resulting exercise IDs compose with list_exercise_body_zones() and * list_exercise_performance(); the latter exposes newest history and explicit * MAX without duplicating either datum for a future session generator. */ TrainlogStatus trainlog_database_list_exercises_filtered(TrainlogDatabase *database, const char *normalized_prefix, const char *zone_id, bool include_descendants, bool primary_only, bool unclassified_only, TrainlogExercise *output, size_t capacity, size_t *output_count); /* CONTRACT: name/profile/direct zones are one transaction and all string/list * inputs are borrowed only for the duration of the call. Profile changes affect * future occurrences only because schema v16 snapshots the complete profile on * every occurrence. Existing sessions and drafts are never rewritten. The * function owns a transaction when called in autocommit mode and participates * in an already-open caller transaction otherwise. Zone rules match * replace_exercise_body_zones(). Unknown exercise IDs return NOT_FOUND; invalid * metadata and name collisions remain explicit errors. */ TrainlogStatus trainlog_database_update_exercise_profiled(TrainlogDatabase *database, const char *exercise_id, const char *name, const char *normalized_name, TrainlogTrackingMode tracking_mode, TrainlogRecordingMode recording_mode, TrainlogExerciseDataFields data_fields, const char *primary_zone_id, const char *const *secondary_zone_ids, size_t secondary_count); TrainlogStatus trainlog_database_insert_session(TrainlogDatabase *database, const TrainlogSessionInput *session); TrainlogStatus trainlog_database_session_count(TrainlogDatabase *database, size_t *output_count); /* Observable history only: a returned session owns at least one persisted * performed set, continuous activity, or explicit MAX result. ended_at is * presentation metadata and is not an actual-work gate. */ TrainlogStatus trainlog_database_list_sessions(TrainlogDatabase *database, TrainlogSessionSummary *output, size_t capacity, size_t *output_count); TrainlogStatus trainlog_database_insert_body_observation(TrainlogDatabase *database, const TrainlogBodyObservationInput *observation); TrainlogStatus trainlog_database_list_weight_points(TrainlogDatabase *database, TrainlogWeightPoint *output, size_t capacity, size_t *output_count); /* TRAINLOG_SESSION_DETAILS_API */ #define TRAINLOG_SET_SUMMARY_MAX 1024U typedef struct TrainlogPersistedExerciseDetail { /* Stable occurrence identity; exercise_id is catalogue identity only. */ char entry_id[TRAINLOG_ID_MAX + 1U]; char name[TRAINLOG_NAME_MAX + 1U]; char equipment_id[TRAINLOG_ID_MAX + 1U]; TrainlogTrackingMode tracking_mode; TrainlogRecordingMode recording_mode; TrainlogExerciseDataFields data_fields; TrainlogLoadMode load_mode; int rest_seconds; int target_sets; int target_reps; int target_duration_seconds; int has_target_weight; double target_weight_kg; int has_max_weight; double max_weight_kg; int continuous_duration_seconds; int has_continuous_speed; double continuous_speed_kmh; int has_continuous_distance; double continuous_distance_km; size_t actual_set_count; /* CONTRACT: history consumes this occurrence-owned ordered snapshot; * rendering must not reach into SQLite or impose a global dataset cap. */ TrainlogSetInput *actual_sets; char actual_summary[TRAINLOG_SET_SUMMARY_MAX + 1U]; } TrainlogPersistedExerciseDetail; /** * @brief Load one session header plus ordered exercise details. * * Callers must release a prior successful result before reusing its array. * On success, each copied detail owns actual_sets until released with * trainlog_database_free_session_details(). On failure, the function releases * every partial allocation and, when output_exercise_count is non-NULL, sets * it to zero. A nonzero capacity requires a non-NULL output_exercises array. */ TrainlogStatus trainlog_database_get_session_details(TrainlogDatabase *database, const char *session_id, TrainlogSessionSummary *output_session, TrainlogPersistedExerciseDetail *output_exercises, size_t exercise_capacity, size_t *output_exercise_count); /* Safe for zero-initialized details and details returned by the loader. */ void trainlog_database_free_session_details(TrainlogPersistedExerciseDetail *exercises, size_t exercise_count); /* TRAINLOG_BODY_METRIC_HISTORY_API */ typedef enum TrainlogBodyMetric { TRAINLOG_BODY_METRIC_WEIGHT = 0, TRAINLOG_BODY_METRIC_NECK, TRAINLOG_BODY_METRIC_SHOULDERS, TRAINLOG_BODY_METRIC_CHEST, TRAINLOG_BODY_METRIC_WAIST, TRAINLOG_BODY_METRIC_HIPS, TRAINLOG_BODY_METRIC_LEFT_ARM, TRAINLOG_BODY_METRIC_RIGHT_ARM, TRAINLOG_BODY_METRIC_LEFT_FOREARM, TRAINLOG_BODY_METRIC_RIGHT_FOREARM, TRAINLOG_BODY_METRIC_LEFT_THIGH, TRAINLOG_BODY_METRIC_RIGHT_THIGH, TRAINLOG_BODY_METRIC_LEFT_CALF, TRAINLOG_BODY_METRIC_RIGHT_CALF, TRAINLOG_BODY_METRIC_COUNT } TrainlogBodyMetric; typedef struct TrainlogBodyMetricPoint { char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U]; double value; } TrainlogBodyMetricPoint; typedef struct TrainlogBodyPairPoint { bool found; char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U]; double left_value; double right_value; } TrainlogBodyPairPoint; TrainlogStatus trainlog_database_list_body_metric_points(TrainlogDatabase *database, TrainlogBodyMetric metric, TrainlogBodyMetricPoint *output, size_t capacity, size_t *output_count); TrainlogStatus trainlog_database_latest_body_pair(TrainlogDatabase *database, TrainlogBodyMetric left_metric, TrainlogBodyMetric right_metric, TrainlogBodyPairPoint *output); /* TRAINLOG_EXERCISE_PERFORMANCE_API */ typedef struct TrainlogExercisePerformancePoint { char session_id[TRAINLOG_ID_MAX + 1U]; char started_at[TRAINLOG_TIMESTAMP_MAX + 1U]; TrainlogSessionType session_type; TrainlogTrackingMode tracking_mode; TrainlogLoadMode load_mode; size_t actual_set_count; int has_performance; /* Explicit schema-v9 max: weight is the result; metric_value is internal. */ int has_explicit_max; int metric_value; int has_weight; double weight_kg; char equipment_id[TRAINLOG_ID_MAX + 1U]; } TrainlogExercisePerformancePoint; /** * @brief Read newest-first per-session representative performance. * * Representative-set semantics: * * - no load: greatest successful reps/duration; * - external load: greatest load, then greatest reps/duration; * - assistance: lowest assistance, then greatest reps/duration. * * A zero-repetition failed attempt is never promoted to representative * performance. This API does not create or infer a measured maximum. */ TrainlogStatus trainlog_database_list_exercise_performance(TrainlogDatabase *database, const char *exercise_id, TrainlogExercisePerformancePoint *output, size_t capacity, size_t *output_count); /* TRAINING_KNOWLEDGE_RUNTIME_READ_V1 */ #define TRAINLOG_OCCURRENCE_PAGE_MAX 32U #define TRAINLOG_OCCURRENCE_SET_PAGE_MAX 64U typedef struct TrainlogExerciseOccurrenceCursor { char started_at[TRAINLOG_TIMESTAMP_MAX + 1U]; char session_id[TRAINLOG_ID_MAX + 1U]; char entry_id[TRAINLOG_ID_MAX + 1U]; } TrainlogExerciseOccurrenceCursor; typedef struct TrainlogExerciseOccurrence { char session_id[TRAINLOG_ID_MAX + 1U]; char entry_id[TRAINLOG_ID_MAX + 1U]; char exercise_id[TRAINLOG_ID_MAX + 1U]; char started_at[TRAINLOG_TIMESTAMP_MAX + 1U]; char equipment_id[TRAINLOG_ID_MAX + 1U]; TrainlogSessionType session_type; TrainlogTrackingMode tracking_mode; TrainlogRecordingMode recording_mode; TrainlogExerciseDataFields data_fields; TrainlogLoadMode load_mode; size_t set_count; int continuous_duration_seconds; bool continuous_has_speed; double continuous_speed_kmh; bool continuous_has_distance; double continuous_distance_km; } TrainlogExerciseOccurrence; typedef struct TrainlogOccurrenceSet { size_t position; bool has_reps; int reps; bool has_duration; int duration_seconds; bool has_weight; double weight_kg; } TrainlogOccurrenceSet; typedef struct TrainlogLatestExplicitMax { bool found; char session_id[TRAINLOG_ID_MAX + 1U]; char entry_id[TRAINLOG_ID_MAX + 1U]; char started_at[TRAINLOG_TIMESTAMP_MAX + 1U]; char equipment_id[TRAINLOG_ID_MAX + 1U]; TrainlogLoadMode load_mode; double max_weight_kg; } TrainlogLatestExplicitMax; /* Exact-ID profile read; unknown IDs return NOT_FOUND. */ TrainlogStatus trainlog_database_get_exercise_profile(TrainlogDatabase *database, const char *exercise_id, TrainlogExercise *output); /** * Current-data keyset page ordered by the exact started_at instant, then * session_id and entry_id bytewise DESC. Accepted timestamps use the frozen * extended RFC3339 grammar, including T/t, Z/z, numeric offsets through * 23:59, arbitrary fractional precision, and the Android writer's omitted * seconds form. Equivalent trailing-zero fractions represent one instant. * A non-NULL cursor is exclusive. Pages are not a cross-call snapshot under * concurrent edits. Cursor arrays are borrowed for this call, must be NUL * terminated within their declared capacities, and started_at must be a real * accepted instant with an explicit numeric offset or Z/z. Malformed cursors * are INVALID_ARGUMENT before any history query. Malformed matching persisted * timestamps, or a selected timestamp too long for the fixed public output, * are DATABASE_ERROR rather than omitted or truncated. limit must be * 1..TRAINLOG_OCCURRENCE_PAGE_MAX. */ TrainlogStatus trainlog_database_list_exercise_occurrences_page(TrainlogDatabase *database, const char *exercise_id, const TrainlogExerciseOccurrenceCursor *after, size_t limit, TrainlogExerciseOccurrence *output, size_t *output_count, bool *output_has_more, TrainlogExerciseOccurrenceCursor *output_next); /* Original set positions are preserved. after_position is exclusive; use -1 * for the first page. Reps are non-negative, so zero preserves a failed * attempt. Nullable values remain distinct from zero. Corrupt storage types or * values outside the public int/size_t ranges return DATABASE_ERROR. No rows * exist for continuous occurrences. Output remains caller-owned. */ TrainlogStatus trainlog_database_list_occurrence_sets_page(TrainlogDatabase *database, const char *entry_id, int after_position, size_t limit, TrainlogOccurrenceSet *output, size_t *output_count, bool *output_has_more, int *output_next_position); /* Reads max_results joined only through max_test sessions; ordinary large sets * never qualify. It uses the same exact instant and bytewise-ID ordering as * occurrence pagination. Output keeps the exact source timestamp and * occurrence load/equipment context; corrupt or over-capacity selected storage * returns DATABASE_ERROR. */ TrainlogStatus trainlog_database_latest_explicit_max_context(TrainlogDatabase *database, const char *exercise_id, TrainlogLatestExplicitMax *output); /* Same chronological contract, additionally restricted to one exact nonempty * equipment ID. This is the compatibility reader for user-directed %MAX. */ TrainlogStatus trainlog_database_latest_explicit_max_equipment_context(TrainlogDatabase *database, const char *exercise_id, const char *equipment_id, TrainlogLatestExplicitMax *output); /* TRAINLOG_SESSION_EDIT_API */ typedef struct TrainlogEditableExerciseRecord { char entry_id[TRAINLOG_ID_MAX + 1U]; char exercise_id[TRAINLOG_ID_MAX + 1U]; char equipment_id[TRAINLOG_ID_MAX + 1U]; char name[TRAINLOG_NAME_MAX + 1U]; TrainlogTrackingMode tracking_mode; TrainlogRecordingMode recording_mode; TrainlogExerciseDataFields data_fields; TrainlogLoadMode load_mode; int rest_seconds; int target_sets; int target_reps; int target_duration_seconds; int has_target_weight; double target_weight_kg; int has_max_weight; double max_weight_kg; int continuous_duration_seconds; int has_continuous_speed; double continuous_speed_kmh; int has_continuous_distance; double continuous_distance_km; char notes[TRAINLOG_NOTE_MAX + 1U]; size_t set_offset; size_t set_count; } TrainlogEditableExerciseRecord; /** * @brief Load a complete persisted session into caller-owned editable buffers. * * The function never truncates a session silently. If either caller capacity is * too small it returns TRAINLOG_STATUS_INVALID_ARGUMENT and the outputs must not * be used. */ TrainlogStatus trainlog_database_load_session_editable(TrainlogDatabase *database, const char *session_id, TrainlogSessionSummary *output_session, TrainlogEditableExerciseRecord *output_exercises, size_t exercise_capacity, size_t *output_exercise_count, TrainlogSetInput *output_sets, size_t set_capacity, size_t *output_set_count); /** * @brief Replace only the exercise/set contents of an existing session. * * The session row itself is preserved, so session_id, timestamps, session type, * session notes, session follow-ups and body_observations.session_row_id remain * attached to the same row. Exercise feedback is remapped only by retained * entry_id within that session; removed entries cascade and new entries never * inherit feedback. The replacement is atomic: any error rolls the whole * operation back. */ TrainlogStatus trainlog_database_replace_session_exercises(TrainlogDatabase *database, const char *session_id, const TrainlogSessionExerciseInput *exercises, size_t exercise_count); /* TRAINLOG_BODY_OBSERVATION_RECORD_API */ typedef struct TrainlogBodyObservationRecord { char observation_id[TRAINLOG_ID_MAX + 1U]; char observed_at[TRAINLOG_TIMESTAMP_MAX + 1U]; char session_id[TRAINLOG_ID_MAX + 1U]; bool has_body_weight; double body_weight_kg; bool has_neck; double neck_cm; bool has_shoulders; double shoulders_cm; bool has_chest; double chest_cm; bool has_waist; double waist_cm; bool has_hips; double hips_cm; bool has_left_arm; double left_arm_cm; bool has_right_arm; double right_arm_cm; bool has_left_forearm; double left_forearm_cm; bool has_right_forearm; double right_forearm_cm; bool has_left_thigh; double left_thigh_cm; bool has_right_thigh; double right_thigh_cm; bool has_left_calf; double left_calf_cm; bool has_right_calf; double right_calf_cm; char notes[TRAINLOG_NOTE_MAX + 1U]; } TrainlogBodyObservationRecord; TrainlogStatus trainlog_database_list_body_observations(TrainlogDatabase *database, TrainlogBodyObservationRecord *output, size_t capacity, size_t *output_count); TrainlogStatus trainlog_database_get_body_observation(TrainlogDatabase *database, const char *observation_id, TrainlogBodyObservationRecord *output); /** * @brief Correct metric values of one existing observation. * * observation_id identifies the stable row. observed_at and session linkage * remain unchanged. At least one body metric must remain present. */ TrainlogStatus trainlog_database_update_body_observation(TrainlogDatabase *database, const TrainlogBodyObservationInput *observation); #endif