/**
 * Copyright (c) 2020-present, Goldman Sachs
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import dagre from '@dagrejs/dagre';
import { filterByType } from '@finos/legend-shared';
import { BusinessMilestoning, BusinessSnapshotMilestoning, Column, ProcessingMilestoning, ProcessingSnapshotMilestoning, View, stringifyDataType, } from '@finos/legend-graph';
export const isView = (relation) => relation instanceof View;
/**
 * Stable identifier for a relation within a Database, encoded as
 * `<schema>.<name>`. Schema-qualified to disambiguate same-named relations
 * across schemas. Works for both Tables and Views.
 */
export const getRelationId = (relation) => `${relation.schema.name}.${relation.name}`;
/**
 * Display string for a relational data type (e.g. `VARCHAR(40)`,
 * `DECIMAL(10,4)`). Wraps the upstream helper so the editor doesn't need to
 * know about RelationalDataType subclasses directly.
 */
export const getColumnTypeLabel = (column) => stringifyDataType(column.type);
/**
 * `Relation.columns` is statically typed as `RelationalOperationElement[]`
 * (the broader supertype) but is populated with `Column` instances at runtime
 * for tables. Filter so the form-mode editor can rely on the narrower type
 * without casting at every call site.
 */
export const getTableColumns = (table) => table.columns.filter(filterByType(Column));
/**
 * Whether a column participates in a relation's primary key. Both Tables and
 * Views can declare a primary key (`primaryKey: Column[]`). For Views we
 * compare by name because a view's `columnMappings` carry their column name
 * directly rather than referencing the same `Column` instances.
 */
export const isPrimaryKey = (relation, columnName) => relation.primaryKey.some((pk) => pk.name === columnName);
/**
 * Placeholder text shown while view-column Pure-code formulas are still
 * loading (or if rendering them failed). Centralized so both the canvas
 * table node and the tree column row stay in sync.
 */
export const VIEW_COLUMN_FORMULA_PLACEHOLDER = 'calculate [...]';
/**
 * Resolve the Pure-code formula for a single view column mapping. Looks the
 * pre-rendered formula up by `<schema>.<view>.<column>` key in the map
 * populated by `DatabaseEditorState.loadViewColumnFormulas()`. Falls back to
 * a static placeholder when the formula isn't ready yet (initial load,
 * background re-render) or the engine couldn't render it.
 *
 * The map is passed in (not read directly here) so this module stays
 * framework-agnostic — DatabaseDiagramHelper has no MobX dependency.
 */
export const resolveViewColumnFormula = (formulas, schemaName, viewName, columnName) => formulas.get(`${schemaName}.${viewName}.${columnName}`) ??
    VIEW_COLUMN_FORMULA_PLACEHOLDER;
/**
 * Placeholder text shown while filter Pure-code formulas are still loading
 * (or if rendering them failed). Centralized so the side-panel filter row
 * has a sensible fallback during the brief async window.
 */
export const FILTER_FORMULA_PLACEHOLDER = 'filter [...]';
/**
 * Resolve the Pure-code formula for a single database-level filter. Filter
 * names are unique within a Database, so the lookup key is just the filter
 * name (no schema qualifier needed). Falls back to a static placeholder when
 * the formula isn't ready yet or the engine couldn't render it.
 */
export const resolveFilterFormula = (formulas, filterName) => formulas.get(filterName) ?? FILTER_FORMULA_PLACEHOLDER;
/**
 * Placeholder shown while a join's Pure-code operation is loading or if
 * rendering failed. Same async-load pattern as filters and view-column
 * formulas — reused so the side panel and canvas tooltips stay in sync.
 */
export const JOIN_FORMULA_PLACEHOLDER = 'join [...]';
export const resolveJoinFormula = (formulas, joinName) => formulas.get(joinName) ?? JOIN_FORMULA_PLACEHOLDER;
/**
 * Placeholder shown for view groupBy expressions while the engine render
 * is in flight or if rendering failed. Kept distinct from the column-mapping
 * placeholder so users can tell at a glance which kind of expression is
 * still loading.
 */
export const VIEW_GROUP_BY_FORMULA_PLACEHOLDER = 'group by [...]';
/**
 * Resolve the Pure-code formula for a single view groupBy column expression.
 * Keys mirror the loader in `DatabaseEditorState`:
 * `<schema>.<view>.groupBy[<index>]`. Falls back to the static placeholder
 * when the formula isn't ready yet.
 */
export const resolveViewGroupByFormula = (formulas, schemaName, viewName, index) => formulas.get(`${schemaName}.${viewName}.groupBy[${index}]`) ??
    VIEW_GROUP_BY_FORMULA_PLACEHOLDER;
/**
 * Lowercase, trimmed search-text matcher used by the side-panel tree.
 * Empty query matches everything (so consumers don't need a special case).
 * Match is case-insensitive substring \u2014 not fuzzy \u2014 because users
 * typically know the exact prefix of the schema/relation/column they're
 * looking for and substring keeps the "what matched" obvious.
 */
export const matchesSearch = (name, query) => {
    if (!query) {
        return true;
    }
    return name.toLowerCase().includes(query);
};
export const summarizeMilestoning = (milestoning) => {
    if (milestoning instanceof BusinessMilestoning) {
        const inclusive = milestoning.thruIsInclusive ? ', thru inclusive' : '';
        return {
            label: `business[${milestoning.from}\u2026${milestoning.thru}]`,
            description: `Business milestoning on columns ${milestoning.from} \u2192 ${milestoning.thru}${inclusive}`,
            kind: 'business',
        };
    }
    if (milestoning instanceof BusinessSnapshotMilestoning) {
        return {
            label: `business snapshot(${milestoning.snapshotDate})`,
            description: `Business snapshot milestoning on column ${milestoning.snapshotDate}`,
            kind: 'business',
        };
    }
    if (milestoning instanceof ProcessingMilestoning) {
        const inclusive = milestoning.outIsInclusive ? ', out inclusive' : '';
        return {
            label: `processing[${milestoning.in}\u2026${milestoning.out}]`,
            description: `Processing milestoning on columns ${milestoning.in} \u2192 ${milestoning.out}${inclusive}`,
            kind: 'processing',
        };
    }
    if (milestoning instanceof ProcessingSnapshotMilestoning) {
        return {
            label: `processing snapshot(${milestoning.snapshotDate})`,
            description: `Processing snapshot milestoning on column ${milestoning.snapshotDate}`,
            kind: 'processing',
        };
    }
    // Plugin-defined milestoning kinds: surface the class name so the user
    // at least knows the table is milestoned even if we can't decode it.
    const className = milestoning.constructor.name;
    return {
        label: className,
        description: `Custom milestoning (${className})`,
        kind: 'unknown',
    };
};
/**
 * Whether a join's two endpoints are the same relation (e.g. a hierarchy
 * self-join: `Employee → Employee` on `managerId = id`). We treat any join
 * whose first alias pair has identical source/target relation ids as a
 * self-join — mirrors how `buildJoinEdges` matches endpoints.
 */
export const isSelfJoin = (join) => {
    const firstPair = join.aliases[0];
    if (!firstPair) {
        return false;
    }
    const source = firstPair.first.relation.value;
    const target = firstPair.second.relation.value;
    return (source.schema.name === target.schema.name && source.name === target.name);
};
/**
 * Whether either endpoint of a join lives outside `database` (i.e. resolves
 * to a relation owned by another, included, store). Used to surface a
 * "CROSS-DB" badge in the side panel and to render the canvas placeholder
 * node for the foreign endpoint.
 */
export const isCrossDatabaseJoin = (join, database) => {
    const firstPair = join.aliases[0];
    if (!firstPair) {
        return false;
    }
    const sourceOwner = firstPair.first.relation.value.schema._OWNER;
    const targetOwner = firstPair.second.relation.value.schema._OWNER;
    return sourceOwner !== database || targetOwner !== database;
};
/**
 * Number of column rows a relation's table-node will render. Tables expose
 * Column[] via `columns`, Views expose ColumnMapping[] via `columnMappings`.
 */
export const getRelationColumnCount = (relation) => isView(relation)
    ? relation.columnMappings.length
    : getTableColumns(relation).length;
const collectColumnsFromOperation = (
// The operation tree includes DynaFunction with parameters, TableAliasColumn,
// Literal, etc. We descend it loosely to collect any column references.
operation, acc) => {
    if (operation === undefined) {
        return;
    }
    // TableAliasColumn has a `column.value: Column`
    const candidate = operation;
    if (candidate.column?.value) {
        acc.add(candidate.column.value);
    }
    if (Array.isArray(candidate.parameters)) {
        candidate.parameters.forEach((p) => collectColumnsFromOperation(p, acc));
    }
};
/**
 * Identify columns that participate in any join in the database. Used to badge
 * columns as foreign keys in the table node.
 *
 * Note: in Pure relational, joins ARE the relationships — there is no separate
 * FK constraint on the column. A column is "FK-like" iff some join's operation
 * references it.
 */
export const collectForeignKeyColumns = (database) => {
    const fkColumns = new Set();
    database.joins.forEach((join) => {
        collectColumnsFromOperation(join.operation, fkColumns);
    });
    return fkColumns;
};
/** Synthetic relation id used for the placeholder node that stands in for a
 *  cross-database join's foreign endpoint. Includes the schema-qualified
 *  source path so the same foreign relation is reused across multiple joins
 *  rather than producing one stub per join. */
export const getForeignRelationStubId = (ownerPath, schemaName, relationName) => `__foreign__:${ownerPath}::${schemaName}.${relationName}`;
/**
 * Walk all joins in the database and produce a deduplicated list of edges
 * between relations (tables and/or views), including self-joins (rendered
 * as loop edges) and cross-database joins (whose foreign endpoint becomes
 * a placeholder stub).
 *
 * Implementation notes:
 * - `Join.aliases` typically contains both directions `(A→B)` and `(B→A)` as a
 *   lookup optimization. We treat A↔B as a single edge and use the first alias.
 * - Self-joins (A↔A) are kept and flagged via `isSelfJoin` so the canvas can
 *   render a loop edge instead of skipping them.
 * - Joins whose endpoints reference relations outside this database (typically
 *   through `includes` / `includedStoreSpecifications`) are kept and flagged
 *   via `isCrossDatabase`. The foreign endpoint is replaced with a stub id so
 *   the caller can render a small placeholder node beside the in-DB endpoint.
 */
export const buildJoinEdges = (database) => {
    const ownIds = new Set();
    database.schemas.forEach((schema) => {
        schema.tables.forEach((table) => ownIds.add(getRelationId(table)));
        schema.views.forEach((view) => ownIds.add(getRelationId(view)));
    });
    const edges = [];
    const stubs = new Map();
    database.joins.forEach((join) => {
        const firstPair = join.aliases[0];
        if (!firstPair) {
            return;
        }
        const sourceRelation = firstPair.first.relation.value;
        const targetRelation = firstPair.second.relation.value;
        const rawSourceId = `${sourceRelation.schema.name}.${sourceRelation.name}`;
        const rawTargetId = `${targetRelation.schema.name}.${targetRelation.name}`;
        const sourceInOwn = ownIds.has(rawSourceId);
        const targetInOwn = ownIds.has(rawTargetId);
        // Resolve each endpoint to either its real node id (when in-DB) or a
        // stub id (when foreign). For pure self-joins we keep both ids as the
        // same real id so React Flow draws a loop on that node.
        let sourceId = rawSourceId;
        if (!sourceInOwn) {
            const ownerPath = sourceRelation.schema._OWNER.path;
            const stubId = getForeignRelationStubId(ownerPath, sourceRelation.schema.name, sourceRelation.name);
            sourceId = stubId;
            if (!stubs.has(stubId)) {
                stubs.set(stubId, {
                    id: stubId,
                    schemaName: sourceRelation.schema.name,
                    relationName: sourceRelation.name,
                    ownerPath,
                });
            }
        }
        let targetId = rawTargetId;
        if (!targetInOwn) {
            const ownerPath = targetRelation.schema._OWNER.path;
            const stubId = getForeignRelationStubId(ownerPath, targetRelation.schema.name, targetRelation.name);
            targetId = stubId;
            if (!stubs.has(stubId)) {
                stubs.set(stubId, {
                    id: stubId,
                    schemaName: targetRelation.schema.name,
                    relationName: targetRelation.name,
                    ownerPath,
                });
            }
        }
        edges.push({
            id: `join:${join.name}`,
            name: join.name,
            source: sourceId,
            target: targetId,
            join,
            isSelfJoin: sourceInOwn && targetInOwn && sourceId === targetId,
            isCrossDatabase: !sourceInOwn || !targetInOwn,
        });
    });
    return { edges, foreignStubs: Array.from(stubs.values()) };
};
const NODE_WIDTH = 240;
const NODE_HEADER_HEIGHT = 36;
const NODE_COL_HEIGHT = 22;
const NODE_PADDING = 8;
export const estimateNodeHeight = (relation) => NODE_HEADER_HEIGHT +
    getRelationColumnCount(relation) * NODE_COL_HEIGHT +
    NODE_PADDING;
/**
 * Run dagre on the relation/edge graph and return positions keyed by
 * relation id. Uses left-to-right layering, which suits ERDs better than
 * top-down.
 */
export const layoutDatabaseDiagram = (nodes, edges) => {
    const g = new dagre.graphlib.Graph();
    g.setDefaultEdgeLabel(() => ({}));
    g.setGraph({
        rankdir: 'LR',
        nodesep: 60,
        ranksep: 120,
        marginx: 20,
        marginy: 20,
    });
    nodes.forEach((node) => {
        g.setNode(node.id, { width: NODE_WIDTH, height: node.estimatedHeight });
    });
    edges.forEach((edge) => {
        g.setEdge(edge.source, edge.target);
    });
    dagre.layout(g);
    const out = new Map();
    nodes.forEach((node) => {
        const laidOut = g.node(node.id);
        if (laidOut) {
            // dagre returns center; React Flow expects top-left.
            out.set(node.id, {
                id: node.id,
                x: laidOut.x - laidOut.width / 2,
                y: laidOut.y - laidOut.height / 2,
            });
        }
        else {
            out.set(node.id, { id: node.id, x: 0, y: 0 });
        }
    });
    return out;
};
/**
 * Flat list of (schema, relation) pairs in deterministic order — used by the
 * canvas builder. Tables come before views within each schema (alphabetic
 * within each kind), so the canvas layout stays stable as a database grows.
 */
export const getOrderedRelations = (database) => {
    const pairs = [];
    database.schemas
        .slice()
        .sort((a, b) => a.name.localeCompare(b.name))
        .forEach((schema) => {
        schema.tables
            .slice()
            .sort((a, b) => a.name.localeCompare(b.name))
            .forEach((table) => pairs.push({ schema, relation: table }));
        schema.views
            .slice()
            .sort((a, b) => a.name.localeCompare(b.name))
            .forEach((view) => pairs.push({ schema, relation: view }));
    });
    return pairs;
};
//# sourceMappingURL=DatabaseDiagramHelper.js.map