import { nonexhaustive } from "@likec4/core";
import {
  AstUtils,
  DefaultScopeProvider,
  DONE_RESULT,
  EMPTY_SCOPE,
  EMPTY_STREAM,
  MapScope,
  stream,
  StreamImpl,
  StreamScope
} from "langium";
import { ast } from "../ast.js";
import { logger } from "../logger.js";
import { elementRef, getFqnElementRef } from "../utils/elementRef.js";
const { getDocument } = AstUtils;
export class LikeC4ScopeProvider extends DefaultScopeProvider {
  deploymentsIndex;
  fqnIndex;
  constructor(services) {
    super(services);
    this.fqnIndex = services.likec4.FqnIndex;
    this.deploymentsIndex = services.likec4.DeploymentsIndex;
  }
  directChildrenOf(parent) {
    return this.fqnIndex.directChildrenOf(parent);
  }
  // we need lazy resolving here
  uniqueDescedants(of) {
    return new StreamImpl(
      () => {
        const element = of();
        const fqn = element && this.fqnIndex.getFqn(element);
        if (fqn) {
          return this.fqnIndex.uniqueDescedants(fqn).iterator();
        }
        return null;
      },
      (iterator) => {
        if (iterator) {
          return iterator.next();
        }
        return DONE_RESULT;
      }
    );
  }
  scopeElementRef(ref) {
    return this.uniqueDescedants(() => ref.el.ref);
  }
  scopeExtendElement({ element }) {
    return stream([element.el.$nodeDescription]).nonNullable().concat(this.uniqueDescedants(() => elementRef(element)));
  }
  scopeElementView({ viewOf, extends: ext }) {
    if (viewOf) {
      return stream([viewOf.el.$nodeDescription]).nonNullable().concat(this.uniqueDescedants(() => elementRef(viewOf)));
    }
    if (ext) {
      return stream([ext]).flatMap((v) => {
        const view = v.view.ref;
        return view ? this.scopeElementView(view) : EMPTY_STREAM;
      });
    }
    return EMPTY_STREAM;
  }
  getScope(context) {
    try {
      const referenceType = this.reflection.getReferenceType(context);
      try {
        const container = context.container;
        if (ast.isFqnRef(container)) {
          return this.getScopeForFqnRef(container, context);
        }
        if (referenceType !== ast.Element) {
          return this.getGlobalScope(referenceType, context);
        }
        if (ast.isFqnElementRef(container) && context.property === "el") {
          const parent = container.parent;
          if (!parent) {
            return this.getGlobalScope(referenceType, context);
          }
          return new StreamScope(this.directChildrenOf(getFqnElementRef(parent)));
        }
        if (ast.isElementRef(container) && context.property === "el") {
          const parent = container.parent;
          if (parent) {
            return new StreamScope(this.scopeElementRef(parent));
          }
          if (context.reference.$refText === "this" || context.reference.$refText === "it") {
            const closestElement = AstUtils.getContainerOfType(container, ast.isElement);
            if (closestElement) {
              return new MapScope([
                this.descriptions.createDescription(closestElement, context.reference.$refText)
              ]);
            } else {
              return EMPTY_SCOPE;
            }
          }
        }
        return this.computeScope(context);
      } catch (e) {
        logger.warn(e);
        return this.getGlobalScope(referenceType, context);
      }
    } catch (e) {
      logger.warn(e);
      return EMPTY_SCOPE;
    }
  }
  getScopeForFqnRef(container, context) {
    const parent = container.parent;
    if (!parent) {
      return this.createScope(
        // First preference for deployment nodes
        this.computeScope(context, ast.DeploymentNode).getAllElements(),
        this.createScope(
          // Second preference for deployed instances
          this.computeScope(context, ast.DeployedInstance).getAllElements(),
          // Third preference for elements if we are in deployment view
          AstUtils.hasContainerOfType(container, ast.isDeploymentView) ? this.computeScope(context, ast.Element) : EMPTY_SCOPE
        )
      );
    }
    const parentRef = parent.value.ref;
    if (!parentRef) {
      return EMPTY_SCOPE;
    }
    if (ast.isDeploymentNode(parentRef)) {
      return new StreamScope(this.deploymentsIndex.nested(parentRef));
    }
    if (ast.isDeployedInstance(parentRef)) {
      return new StreamScope(this.scopeElementRef(parentRef.element));
    }
    if (ast.isElement(parentRef)) {
      return new StreamScope(this.uniqueDescedants(() => parentRef));
    }
    return nonexhaustive(parentRef);
  }
  computeScope(context, referenceType = this.reflection.getReferenceType(context)) {
    const isElementReference = this.reflection.isSubtype(referenceType, ast.Element);
    const scopes = [];
    const doc = getDocument(context.container);
    const precomputed = doc.precomputedScopes;
    if (!precomputed) {
      return this.getGlobalScope(referenceType, context);
    }
    const byReferenceType = (desc) => this.reflection.isSubtype(desc.type, referenceType);
    let container = context.container;
    while (container) {
      const elements = precomputed.get(container).filter(byReferenceType);
      if (elements.length > 0) {
        scopes.push(stream(elements));
      }
      if (isElementReference && ast.isExtendElementBody(container)) {
        scopes.push(this.scopeExtendElement(container.$container));
      }
      if (isElementReference && ast.isElementViewBody(container)) {
        scopes.push(this.scopeElementView(container.$container));
      }
      container = container.$container;
    }
    return scopes.reduceRight((outerScope, elements) => {
      return this.createScope(elements, outerScope);
    }, this.getGlobalScope(referenceType, context));
  }
}
