import { invariant, isNonEmptyArray, nonexhaustive } from "@likec4/core";
import { isArray, isDefined, isNonNullish, isTruthy } from "remeda";
import {
  ast,
  toAutoLayout,
  toColor,
  toElementStyle,
  ViewOps
} from "../../ast.js";
import { logger, logWarnError } from "../../logger.js";
import { stringHash } from "../../utils/index.js";
import { elementRef } from "../../utils/elementRef.js";
import { parseViewManualLayout } from "../../view-utils/manual-layout.js";
import { removeIndent, toSingleLine } from "./Base.js";
export function ViewsParser(B) {
  return class ViewsParser extends B {
    parseViews() {
      const isValid = this.isValid;
      for (const viewBlock of this.doc.parseResult.value.views) {
        const localStyles = viewBlock.styles.flatMap((s) => {
          try {
            return isValid(s) ? this.parseViewRuleStyleOrGlobalRef(s) : [];
          } catch (e) {
            logWarnError(e);
            return [];
          }
        });
        for (const view of viewBlock.views) {
          try {
            if (!isValid(view)) {
              continue;
            }
            switch (true) {
              case ast.isElementView(view):
                this.doc.c4Views.push(this.parseElementView(view, localStyles));
                break;
              case ast.isDynamicView(view):
                this.doc.c4Views.push(this.parseDynamicElementView(view, localStyles));
                break;
              case ast.isDeploymentView(view):
                this.doc.c4Views.push(this.parseDeploymentView(view));
                break;
              default:
                nonexhaustive(view);
            }
          } catch (e) {
            logWarnError(e);
          }
        }
      }
    }
    parseElementView(astNode, additionalStyles) {
      const body = astNode.body;
      invariant(body, "ElementView body is not defined");
      const astPath = this.getAstNodePath(astNode);
      let viewOf = null;
      if ("viewOf" in astNode) {
        const viewOfEl = elementRef(astNode.viewOf);
        const _viewOf = viewOfEl && this.resolveFqn(viewOfEl);
        if (!_viewOf) {
          logger.warn("viewOf is not resolved: " + astNode.$cstNode?.text);
        } else {
          viewOf = _viewOf;
        }
      }
      let id = astNode.name;
      if (!id) {
        id = "view_" + stringHash(
          this.doc.uri.toString(),
          astPath,
          viewOf ?? ""
        );
      }
      const title = toSingleLine(body.props.find((p) => p.key === "title")?.value) ?? null;
      const description = removeIndent(body.props.find((p) => p.key === "description")?.value) ?? null;
      const tags = this.convertTags(body);
      const links = this.convertLinks(body);
      const manualLayout = parseViewManualLayout(astNode);
      const view = {
        __: "element",
        id,
        astPath,
        title,
        description,
        tags,
        links: isNonEmptyArray(links) ? links : null,
        rules: [
          ...additionalStyles,
          ...body.rules.flatMap((n) => {
            try {
              return this.isValid(n) ? this.parseViewRule(n) : [];
            } catch (e) {
              logWarnError(e);
              return [];
            }
          })
        ],
        ...viewOf && { viewOf },
        ...manualLayout && { manualLayout }
      };
      ViewOps.writeId(astNode, view.id);
      if ("extends" in astNode) {
        const extendsView = astNode.extends.view.ref;
        invariant(extendsView?.name, "view extends is not resolved: " + astNode.$cstNode?.text);
        return Object.assign(view, {
          extends: extendsView.name
        });
      }
      return view;
    }
    parseViewRule(astRule) {
      if (ast.isViewRulePredicate(astRule)) {
        return this.parseViewRulePredicate(astRule);
      }
      if (ast.isViewRuleGlobalPredicateRef(astRule)) {
        return this.parseViewRuleGlobalPredicateRef(astRule);
      }
      if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
        return this.parseViewRuleStyleOrGlobalRef(astRule);
      }
      if (ast.isViewRuleAutoLayout(astRule)) {
        return toAutoLayout(astRule);
      }
      if (ast.isViewRuleGroup(astRule)) {
        return this.parseViewRuleGroup(astRule);
      }
      nonexhaustive(astRule);
    }
    parseViewRulePredicate(astNode) {
      const exprs = [];
      let predicate = astNode.predicates;
      while (predicate) {
        const { value, prev } = predicate;
        try {
          if (isTruthy(value) && this.isValid(value)) {
            exprs.unshift(this.parsePredicate(value));
          }
        } catch (e) {
          logWarnError(e);
        }
        if (!prev) {
          break;
        }
        predicate = prev;
      }
      return ast.isIncludePredicate(astNode) ? { include: exprs } : { exclude: exprs };
    }
    parseViewRuleGlobalPredicateRef(astRule) {
      return {
        predicateId: astRule.predicate.$refText
      };
    }
    parseViewRuleStyleOrGlobalRef(astRule) {
      if (ast.isViewRuleStyle(astRule)) {
        return this.parseViewRuleStyle(astRule);
      }
      if (ast.isViewRuleGlobalStyle(astRule)) {
        return this.parseViewRuleGlobalStyle(astRule);
      }
      nonexhaustive(astRule);
    }
    parseViewRuleGroup(astNode) {
      const groupRules = [];
      for (const rule of astNode.groupRules) {
        try {
          if (!this.isValid(rule)) {
            continue;
          }
          if (ast.isViewRulePredicate(rule)) {
            groupRules.push(this.parseViewRulePredicate(rule));
            continue;
          }
          if (ast.isViewRuleGroup(rule)) {
            groupRules.push(this.parseViewRuleGroup(rule));
            continue;
          }
          nonexhaustive(rule);
        } catch (e) {
          logWarnError(e);
        }
      }
      return {
        title: toSingleLine(astNode.title) ?? null,
        groupRules,
        ...toElementStyle(astNode.props, this.isValid)
      };
    }
    parseViewRuleStyle(astRule) {
      const styleProps = astRule.props.filter(ast.isStyleProperty);
      const targets = astRule.target;
      const notation = astRule.props.find(ast.isNotationProperty);
      return this.parseRuleStyle(styleProps, targets, notation);
    }
    parseRuleStyle(styleProperties, elementExpressionsIterator, notationProperty) {
      const styleProps = toElementStyle(styleProperties, this.isValid);
      const notation = removeIndent(notationProperty?.value);
      const targets = this.parseElementExpressionsIterator(elementExpressionsIterator);
      return {
        targets,
        ...notation && { notation },
        style: {
          ...styleProps
        }
      };
    }
    parseViewRuleGlobalStyle(astRule) {
      return {
        styleId: astRule.style.$refText
      };
    }
    parseDynamicElementView(astNode, additionalStyles) {
      const body = astNode.body;
      invariant(body, "DynamicElementView body is not defined");
      const isValid = this.isValid;
      const props = body.props.filter(isValid);
      const astPath = this.getAstNodePath(astNode);
      let id = astNode.name;
      if (!id) {
        id = "dynamic_" + stringHash(
          this.doc.uri.toString(),
          astPath
        );
      }
      const title = toSingleLine(props.find((p) => p.key === "title")?.value) ?? null;
      const description = removeIndent(props.find((p) => p.key === "description")?.value) ?? null;
      const tags = this.convertTags(body);
      const links = this.convertLinks(body);
      ViewOps.writeId(astNode, id);
      const manualLayout = parseViewManualLayout(astNode);
      return {
        __: "dynamic",
        id,
        astPath,
        title,
        description,
        tags,
        links: isNonEmptyArray(links) ? links : null,
        rules: [
          ...additionalStyles,
          ...body.rules.flatMap((n) => {
            try {
              return isValid(n) ? this.parseDynamicViewRule(n) : [];
            } catch (e) {
              logWarnError(e);
              return [];
            }
          }, [])
        ],
        steps: body.steps.reduce((acc, n) => {
          try {
            if (isValid(n)) {
              if (ast.isDynamicViewParallelSteps(n)) {
                acc.push(this.parseDynamicParallelSteps(n));
              } else {
                acc.push(this.parseDynamicStep(n));
              }
            }
          } catch (e) {
            logWarnError(e);
          }
          return acc;
        }, []),
        ...manualLayout && { manualLayout }
      };
    }
    parseDynamicViewRule(astRule) {
      if (ast.isDynamicViewIncludePredicate(astRule)) {
        return this.parseDynamicViewIncludePredicate(astRule);
      }
      if (ast.isDynamicViewGlobalPredicateRef(astRule)) {
        return this.parseViewRuleGlobalPredicateRef(astRule);
      }
      if (ast.isViewRuleStyleOrGlobalRef(astRule)) {
        return this.parseViewRuleStyleOrGlobalRef(astRule);
      }
      if (ast.isViewRuleAutoLayout(astRule)) {
        return toAutoLayout(astRule);
      }
      nonexhaustive(astRule);
    }
    parseDynamicViewIncludePredicate(astRule) {
      const include = [];
      let iter = astRule.predicates;
      while (iter) {
        try {
          if (isNonNullish(iter.value) && this.isValid(iter.value)) {
            const c4expr = this.parseElementPredicate(iter.value);
            include.unshift(c4expr);
          }
        } catch (e) {
          logWarnError(e);
        }
        iter = iter.prev;
      }
      return { include };
    }
    parseDynamicParallelSteps(node) {
      return {
        __parallel: node.steps.map((step) => this.parseDynamicStep(step))
      };
    }
    parseDynamicStep(node) {
      const sourceEl = elementRef(node.source);
      if (!sourceEl) {
        throw new Error("Invalid reference to source");
      }
      const targetEl = elementRef(node.target);
      if (!targetEl) {
        throw new Error("Invalid reference to target");
      }
      let source = this.resolveFqn(sourceEl);
      let target = this.resolveFqn(targetEl);
      const title = removeIndent(node.title) ?? null;
      let step = {
        source,
        target,
        title
      };
      if (node.isBackward) {
        step = {
          source: target,
          target: source,
          title,
          isBackward: true
        };
      }
      if (!isArray(node.custom?.props)) {
        return step;
      }
      for (const prop of node.custom.props) {
        try {
          switch (true) {
            case ast.isRelationNavigateToProperty(prop): {
              const viewId = prop.value.view.ref?.name;
              if (isTruthy(viewId)) {
                step.navigateTo = viewId;
              }
              break;
            }
            case ast.isRelationStringProperty(prop):
            case ast.isNotationProperty(prop):
            case ast.isNotesProperty(prop): {
              if (isDefined(prop.value)) {
                step[prop.key] = removeIndent(prop.value) ?? "";
              }
              break;
            }
            case ast.isArrowProperty(prop): {
              if (isDefined(prop.value)) {
                step[prop.key] = prop.value;
              }
              break;
            }
            case ast.isColorProperty(prop): {
              const value = toColor(prop);
              if (isDefined(value)) {
                step[prop.key] = value;
              }
              break;
            }
            case ast.isLineProperty(prop): {
              if (isDefined(prop.value)) {
                step[prop.key] = prop.value;
              }
              break;
            }
            default:
              nonexhaustive(prop);
          }
        } catch (e) {
          logWarnError(e);
        }
      }
      return step;
    }
  };
}
