content: /*
   * DazzleConf
   * Copyright © 2025 Anand Beh
   *
   * DazzleConf is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation, either version 3 of the License, or
   * (at your option) any later version.
   *
   * DazzleConf is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
   * GNU Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public License
   * along with DazzleConf. If not, see <https://www.gnu.org/licenses/>
   * and navigate to version 3 of the GNU Lesser General Public License.
   */
  
  package space.arim.dazzleconf.backend.yaml;
  
  import org.checkerframework.checker.nullness.qual.Nullable;
  import org.snakeyaml.engine.v2.comments.CommentLine;
  import org.snakeyaml.engine.v2.comments.CommentType;
  import org.snakeyaml.engine.v2.common.FlowStyle;
  import org.snakeyaml.engine.v2.common.ScalarStyle;
  import org.snakeyaml.engine.v2.nodes.CollectionNode;
  import org.snakeyaml.engine.v2.nodes.MappingNode;
  import org.snakeyaml.engine.v2.nodes.Node;
  import org.snakeyaml.engine.v2.nodes.NodeTuple;
  import org.snakeyaml.engine.v2.nodes.ScalarNode;
  import org.snakeyaml.engine.v2.nodes.SequenceNode;
  import org.snakeyaml.engine.v2.nodes.Tag;
  import org.snakeyaml.engine.v2.representer.StandardRepresenter;
  import space.arim.dazzleconf.backend.CommentData;
  import space.arim.dazzleconf.backend.DataEntry;
  import space.arim.dazzleconf.backend.DataList;
  import space.arim.dazzleconf.backend.DataTree;
  import space.arim.dazzleconf.engine.CommentLocation;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  import java.util.Optional;
  import java.util.function.BiConsumer;
  import java.util.regex.Matcher;
  
  final class WriteYaml {
  
      private final StandardRepresenter standardRepresenter;
  
      WriteYaml(StandardRepresenter standardRepresenter) {
          this.standardRepresenter = standardRepresenter;
      }
  
      Node dataTreeToNode(DataTree dataTree) {
          List<NodeTuple> keyValuePairs = new ArrayList<>(dataTree.size());
          dataTree.forEach((key, entry) -> {
              Node keyNode = standardRepresenter.represent(key);
              Node valueNode = entryToNode(entry, false);
              boolean valueIsMultiLineString;
              if (valueNode instanceof ScalarNode) {
                  ScalarStyle scalarStyle = ((ScalarNode) valueNode).getScalarStyle();
                  valueIsMultiLineString = scalarStyle == ScalarStyle.LITERAL || scalarStyle == ScalarStyle.FOLDED;
              } else {
                  valueIsMultiLineString = false;
              }
              setComments(new SetCommentsOnMapEntry(keyNode, valueNode, valueIsMultiLineString), entry.getComments());
              keyValuePairs.add(new NodeTuple(keyNode, valueNode));
          });
          return new MappingNode(Tag.MAP, keyValuePairs, FlowStyle.AUTO);
      }
  
      private Node dataListToNode(DataList dataList) {
          List<Node> nodeList = new ArrayList<>(dataList.size());
          dataList.forEach(elem -> {
              Node elemNode = entryToNode(elem, true);
              setComments(new SetCommentsOnListEntry(elemNode), elem.getComments());
              nodeList.add(elemNode);
          });
          return new SequenceNode(Tag.SEQ, nodeList, FlowStyle.AUTO);
      }
  
      private Node entryToNode(DataEntry entry, boolean insideList) {
          Object value = entry.getValue();
          if (value instanceof DataTree) {
              return dataTreeToNode((DataTree) value);
          }
          if (value instanceof DataList) {
              // Per the documentation of DataEntry, this cast is safe
              return dataListToNode((DataList) value);
          }
          // 1. Inline comments on list elements are incompatible with multi-line strings
          // 2. Strings which are just new lines don't round-trip in YamlBackend, because of footer writing '\n'
          // 3. Strings which end in more than one new line don't round-trip in YamlBackend, because of footer writing '\n'
          if (value instanceof String) {
              String stringValue = (String) value;
              Matcher multiLineMatcher = StandardRepresenter.MULTILINE_PATTERN.matcher(stringValue);
              if (multiLineMatcher.find()) {
                  // 1
                  if (insideList && !entry.getComments(CommentLocation.INLINE).isEmpty()) {
                      return new ScalarNode(Tag.STR, stringValue, ScalarStyle.DOUBLE_QUOTED);
                  }
                  // 2 + 3
                  int expectStart = 0;
                  int nextStartAt;
                  int consecutiveCount = 0;
                  do {
                      if (expectStart != multiLineMatcher.start()) {
                          consecutiveCount = 0;
                      }
                      expectStart = nextStartAt = multiLineMatcher.end();
                      consecutiveCount++;
                  } while (multiLineMatcher.find(nextStartAt));
  
                  if (consecutiveCount == stringValue.length() // 1.
                          || nextStartAt == stringValue.length() && consecutiveCount > 1) { // 2.
                      return new ScalarNode(Tag.STR, stringValue, ScalarStyle.DOUBLE_QUOTED);
                  }
              }
          }
          return standardRepresenter.represent(value);
      }
  
      private interface SetCommentsOn {
          @Nullable Node whereToPlace(CommentLocation location);
      }
  
      final static class SetCommentsOnMapEntry implements SetCommentsOn {
  
          private final Node keyNode;
          private final Node valueNode;
          private final boolean valueIsMultiLineString;
  
          SetCommentsOnMapEntry(Node keyNode, Node valueNode, boolean valueIsMultiLineString) {
              this.keyNode = keyNode;
              this.valueNode = valueNode;
              this.valueIsMultiLineString = valueIsMultiLineString;
          }
  
          @Override
          public @Nullable Node whereToPlace(CommentLocation location) {
              switch (location) {
                  case ABOVE:
                      return keyNode;
                  case INLINE:
                      // Place multiline strings' inline comments on the key, so that it looks like
                      // example: | # comment
                      //   this is
                      //   a multiline
                      //   string
                      if (valueIsMultiLineString) {
                          return keyNode;
                      }
                  case BELOW:
                      // If the entry is a list or a map, we place inline and below comments on the key
                      return valueNode instanceof CollectionNode ? keyNode : valueNode;
              }
              throw new IncompatibleClassChangeError("Unknown comment location " + location);
          }
      }
  
      private static final class SetCommentsOnListEntry implements SetCommentsOn {
  
          private final Node elemNode;
  
          private SetCommentsOnListEntry(Node elemNode) {
              this.elemNode = elemNode;
          }
  
          @Override
          public @Nullable Node whereToPlace(CommentLocation location) {
              switch (location) {
                  case ABOVE:
                  case BELOW:
                      return elemNode;
                  case INLINE:
                      return elemNode instanceof CollectionNode ? null : elemNode;
              }
              throw new IncompatibleClassChangeError("Unknown comment location " + location);
          }
      }
  
      private static void setComments(SetCommentsOn setCommentsOn, CommentData commentData) {
          {
              List<String> commentsAbove = commentData.getAt(CommentLocation.ABOVE);
              if (!commentsAbove.isEmpty()) {
                  Node chosenNode = setCommentsOn.whereToPlace(CommentLocation.ABOVE);
                  setBlockComments(
                          chosenNode, commentsAbove, commentsAbove.size(), Node::setBlockComments
                  );
              }
          }
          List<String> commentsInline = commentData.getAt(CommentLocation.INLINE);
          List<String> commentsBelow = commentData.getAt(CommentLocation.BELOW);
          if (commentsInline.isEmpty() && commentsBelow.isEmpty()) {
              // Fast path
              return;
          }
          if (commentsInline.size() > 1) {
              throw YamlBackend.doesNotSupport("more than one inline comment");
          }
          Node whereToPlace;
          if (!commentsInline.isEmpty() && (whereToPlace = setCommentsOn.whereToPlace(CommentLocation.INLINE)) != null) {
              whereToPlace.setInLineComments(Collections.singletonList(new CommentLine(
                      Optional.empty(), Optional.empty(), " " + commentsInline.get(0), CommentType.IN_LINE
              )));
          }
          if (!commentsBelow.isEmpty() && (whereToPlace = setCommentsOn.whereToPlace(CommentLocation.BELOW)) != null) {
              setBlockComments(whereToPlace, commentsBelow, 1 + commentsBelow.size(), Node::setEndComments);
              // Add a blank line: This line helps differentiate us from later comments (e.g. ABOVE on other entries)
              whereToPlace.getEndComments().add(new CommentLine(
                      Optional.empty(), Optional.empty(), "", CommentType.BLANK_LINE
              ));
          }
      }
  
      private static void setBlockComments(Node node, List<String> comments, int sizeHint,
                                                 BiConsumer<Node, List<CommentLine>> setter) {
          List<CommentLine> snakeBlockComments = new ArrayList<>(sizeHint);
          for (String comment : comments) {
              snakeBlockComments.add(new CommentLine(
                      Optional.empty(), Optional.empty(), " " + comment, CommentType.BLOCK
              ));
          }
          setter.accept(node, snakeBlockComments);
      }
  
  }
  