import { assert } from "../../../shared/src/asserts.js";
import { must } from "../../../shared/src/must.js";
import { planIdSymbol } from "../../../zero-protocol/src/ast.js";
import { PlannerFanIn } from "./planner-fan-in.js";
import { PlannerFanOut } from "./planner-fan-out.js";
import { PlannerGraph } from "./planner-graph.js";
import { PlannerJoin } from "./planner-join.js";
import { PlannerTerminus } from "./planner-terminus.js";
function wireOutput(from, to) {
  switch (from.kind) {
    case "connection":
    case "join":
    case "fan-in":
      from.setOutput(to);
      break;
    case "fan-out":
      from.addOutput(to);
      break;
    case "terminus":
      assert(false, "Terminus nodes cannot have outputs");
  }
}
function buildPlanGraph(ast, model, isRoot, baseConstraints) {
  const graph = new PlannerGraph();
  let nextPlanId = 0;
  const source = graph.addSource(ast.table, model);
  const connection = source.connect(
    ast.orderBy ?? [],
    ast.where,
    isRoot,
    baseConstraints,
    ast.limit
  );
  graph.connections.push(connection);
  let end = connection;
  if (ast.where) {
    end = processCondition(
      ast.where,
      end,
      graph,
      model,
      ast.table,
      () => nextPlanId++
    );
  }
  const terminus = new PlannerTerminus(end);
  wireOutput(end, terminus);
  graph.setTerminus(terminus);
  const subPlans = {};
  if (ast.related) {
    for (const csq of ast.related) {
      const alias = must(
        csq.subquery.alias,
        "Related subquery must have alias"
      );
      const childConstraints = extractConstraint(
        csq.correlation.childField,
        csq.subquery.table
      );
      subPlans[alias] = buildPlanGraph(
        csq.subquery,
        model,
        true,
        childConstraints
      );
    }
  }
  return { plan: graph, subPlans };
}
function processCondition(condition, input, graph, model, parentTable, getPlanId) {
  switch (condition.type) {
    case "simple":
      return input;
    case "and":
      return processAnd(condition, input, graph, model, parentTable, getPlanId);
    case "or":
      return processOr(condition, input, graph, model, parentTable, getPlanId);
    case "correlatedSubquery":
      return processCorrelatedSubquery(
        condition,
        input,
        graph,
        model,
        parentTable,
        getPlanId
      );
  }
}
function processAnd(condition, input, graph, model, parentTable, getPlanId) {
  let end = input;
  for (const subCondition of condition.conditions) {
    end = processCondition(
      subCondition,
      end,
      graph,
      model,
      parentTable,
      getPlanId
    );
  }
  return end;
}
function processOr(condition, input, graph, model, parentTable, getPlanId) {
  const subqueryConditions = condition.conditions.filter(
    (c) => c.type === "correlatedSubquery" || hasCorrelatedSubquery(c)
  );
  if (subqueryConditions.length === 0) {
    return input;
  }
  const fanOut = new PlannerFanOut(input);
  graph.fanOuts.push(fanOut);
  wireOutput(input, fanOut);
  const branches = [];
  for (const subCondition of subqueryConditions) {
    const branch = processCondition(
      subCondition,
      fanOut,
      graph,
      model,
      parentTable,
      getPlanId
    );
    branches.push(branch);
    fanOut.addOutput(branch);
  }
  const fanIn = new PlannerFanIn(branches);
  graph.fanIns.push(fanIn);
  for (const branch of branches) {
    wireOutput(branch, fanIn);
  }
  return fanIn;
}
function processCorrelatedSubquery(condition, input, graph, model, parentTable, getPlanId) {
  const { related } = condition;
  const childTable = related.subquery.table;
  const childSource = graph.hasSource(childTable) ? graph.getSource(childTable) : graph.addSource(childTable, model);
  const childConnection = childSource.connect(
    related.subquery.orderBy ?? [],
    related.subquery.where,
    false,
    void 0,
    // no base constraints for EXISTS/NOT EXISTS
    condition.op === "EXISTS" ? 1 : void 0
  );
  graph.connections.push(childConnection);
  let childEnd = childConnection;
  if (related.subquery.where) {
    childEnd = processCondition(
      related.subquery.where,
      childEnd,
      graph,
      model,
      childTable,
      getPlanId
    );
  }
  const parentConstraint = extractConstraint(
    related.correlation.parentField
  );
  const childConstraint = extractConstraint(
    related.correlation.childField
  );
  const planId = getPlanId();
  condition[planIdSymbol] = planId;
  const isNotExists = condition.op === "NOT EXISTS";
  const manualFlip = condition.flip;
  let flippable;
  let initialType;
  if (isNotExists) {
    flippable = false;
    initialType = "semi";
  } else if (manualFlip === true) {
    flippable = false;
    initialType = "flipped";
  } else if (manualFlip === false) {
    flippable = false;
    initialType = "semi";
  } else {
    flippable = true;
    initialType = "semi";
  }
  const join = new PlannerJoin(
    input,
    childEnd,
    parentConstraint,
    childConstraint,
    flippable,
    planId,
    initialType
  );
  graph.joins.push(join);
  wireOutput(input, join);
  wireOutput(childEnd, join);
  return join;
}
function hasCorrelatedSubquery(condition) {
  if (condition.type === "correlatedSubquery") {
    return true;
  }
  if (condition.type === "and" || condition.type === "or") {
    return condition.conditions.some(hasCorrelatedSubquery);
  }
  return false;
}
function extractConstraint(fields, _tableName) {
  return Object.fromEntries(fields.map((field) => [field, void 0]));
}
function planRecursively(plans, planDebugger, lc) {
  for (const subPlan of Object.values(plans.subPlans)) {
    planRecursively(subPlan, planDebugger, lc);
  }
  plans.plan.plan(planDebugger, lc);
}
function planQuery(ast, model, planDebugger, lc) {
  const plans = buildPlanGraph(ast, model, true);
  planRecursively(plans, planDebugger, lc);
  return applyPlansToAST(ast, plans);
}
function applyToCondition(condition, flippedIds) {
  if (condition.type === "simple") {
    return condition;
  }
  if (condition.type === "correlatedSubquery") {
    const planId = condition[planIdSymbol];
    const shouldFlip = planId !== void 0 && flippedIds.has(planId);
    return {
      ...condition,
      flip: shouldFlip,
      related: {
        ...condition.related,
        subquery: {
          ...condition.related.subquery,
          where: condition.related.subquery.where ? applyToCondition(condition.related.subquery.where, flippedIds) : void 0
        }
      }
    };
  }
  return {
    ...condition,
    conditions: condition.conditions.map((c) => applyToCondition(c, flippedIds))
  };
}
function applyPlansToAST(ast, plans) {
  const flippedIds = /* @__PURE__ */ new Set();
  for (const join of plans.plan.joins) {
    if (join.type === "flipped" && join.planId !== void 0) {
      flippedIds.add(join.planId);
    }
  }
  return {
    ...ast,
    where: ast.where ? applyToCondition(ast.where, flippedIds) : void 0,
    related: ast.related?.map((csq) => {
      const alias = must(
        csq.subquery.alias,
        "Related subquery must have alias"
      );
      const subPlan = plans.subPlans[alias];
      return {
        ...csq,
        subquery: subPlan ? applyPlansToAST(csq.subquery, subPlan) : csq.subquery
      };
    })
  };
}
export {
  applyPlansToAST,
  buildPlanGraph,
  planQuery
};
//# sourceMappingURL=planner-builder.js.map
