content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you 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.
   */
  package org.apache.causeway.commons.internal.debug.xray.graphics;
  
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.Graphics2D;
  import java.util.ArrayList;
  import java.util.LinkedHashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.TreeMap;
  
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal.base._Refs;
  import org.apache.causeway.commons.internal.base._Refs.IntReference;
  import org.apache.causeway.commons.internal.debug.xray.graphics._Graphics.TextBlock;
  import org.apache.causeway.commons.internal.primitives._Ints;
  
  import lombok.Getter;
  import org.jspecify.annotations.NonNull;
  import lombok.RequiredArgsConstructor;
  
  public class SequenceDiagram {
  
      private final Map<String, String> aliases = new TreeMap<>();
  
      private final Map<String, Participant> participantsById = new LinkedHashMap<>();
      private final List<Connection> connections = new ArrayList<>();
      private final List<Lifeline> lifelines = new ArrayList<>();
  
      private Dimension size;
  
      public SequenceDiagram alias(final String id, final String label) {
          aliases.put(id, label);
          return this;
      }
  
      public void enter(final @NonNull String from, final @NonNull String to, final String label) {
          var p0 = participant(from);
          var p1 = participant(to);
          connections.add(newConnection(p0, p1, label, false));
      }
  
      public void exit(final @NonNull String from, final @NonNull String to, final String label) {
          var p1 = participant(to);
          var p0 = participant(from);
          connections.add(newConnection(p0, p1, label, true));
      }
  
      public void enter(final String from, final String to) {
          enter(from, to, null);
      }
  
      public void exit(final String from, final String to) {
          exit(from, to, null);
      }
  
      public void activate(final String participantId) {
          var participant = participant(participantId);
          var latestConnection = latestConnection();
          lifelines.add(new Lifeline(participant, latestConnection));
      }
  
      public void deactivate(final String participantId) {
          var participant = participant(participantId);
          var latestConnection = latestConnection();
          Can.ofCollection(lifelines).reverse().stream()
          .filter(lifeline->lifeline.getParticipant().equals(participant))
          .findFirst()
          .ifPresent(lifeline->lifeline.endAt = latestConnection);
      }
  
      // -- STYLE OVERRIDE
  
      private Color connectionArrowColor;
      private Color connectionLabelColor;
  
      public void setConnectionArrowColor(final Color connectionArrowColor) {
          this.connectionArrowColor = connectionArrowColor;
      }
  
      public void setConnectionLabelColor(final Color connectionLabelColor) {
          this.connectionLabelColor = connectionLabelColor;
      }
  
      // -- HELPER
  
      private Connection newConnection(
              final Participant from,
              final Participant to,
              final String label,
              final boolean dashedLine) {
          return new Connection(
                  connections.size(),
                  from,
                  to,
                  label,
                  dashedLine,
                  getConnectionArrowColor(),
                  getConnectionLabelColor());
      }
  
      private Participant participant(final String participantId) {
          return participantsById
                  .computeIfAbsent(participantId, id->new Participant(aliases.getOrDefault(id, id)));
      }
  
      private Connection latestConnection() {
          return Can.ofCollection(connections).getLast().orElse(null);
      }
  
      private Color getConnectionArrowColor() {
          return connectionArrowColor!=null
                  ? connectionArrowColor
                  : CONNECTION_ARROW_COLOR;
      }
  
      private Color getConnectionLabelColor() {
          return connectionLabelColor!=null
                  ? connectionLabelColor
                  : CONNECTION_LABEL_COLOR;
      }
  
      // -- RENDERING
  
      private final static Color PARTICIPANT_BACKGROUND_COLOR = _Graphics.COLOR_LIGHTER_GREEN;
      private final static Color PARTICIPANT_BORDER_COLOR = _Graphics.COLOR_DARKER_GREEN;
      private final static int PARTICIPANT_MARGIN_H = 20;
      private final static int PARTICIPANT_MARGIN_V = 5;
      private final static int PARTICIPANT_PADDING_H = 8;
      private final static int PARTICIPANT_PADDING_V = 3;
      private final static int PARTICIPANT_LINEGAP = 0;
      private final static int PARTICIPANT_MAX_CHAR_PER_LINE = 26;
      private final static Optional<Font> PARTICIPANT_FONT = _Graphics.lookupFont("Verdana", 12.f);
  
      private final static Color LIFELINE_BACKGROUND_COLOR = Color.WHITE;
      private final static int LIFELINE_WIDTH = 8;
  
      private final static Color CONNECTION_ARROW_COLOR = _Graphics.COLOR_DARKER_RED;
      private final static Color CONNECTION_LABEL_COLOR = Color.BLACK;
      private final static int CONNECTION_MARGIN_V = 12;
      private final static int CONNECTION_LABEL_PADDING_H = 8;
      private final static int CONNECTION_LABEL_PADDING_V = 3;
      private final static int CONNECTION_LABEL_LINEGAP = 0;
      private final static Optional<Font> CONNECTION_FONT = _Graphics.lookupFont("Courier New", 11.f);
  
      @Getter @RequiredArgsConstructor
      private static class Connection {
          final int index;
          final Participant from;
          final Participant to;
          final String label;
          final boolean dashedLine;
  
          final Color arrowColor;
          final Color labelColor;
  
          TextBlock textBlock;
  
          int x_left;
          int x_from;
          int x_to;
  
          int y_top;
          int y_bottom;
          int height;
  
          void layout(final Graphics2D g, final IntReference y_offset, final List<Lifeline> lifelines) {
  
              x_from = from.getX_middle();
              x_to = to.getX_middle();
  
              var fromConnectsLifeline = lifelines.stream()
                      .filter(ll->ll.getParticipant().equals(from))
                      .anyMatch(ll->ll.overlaps(this));
  
              var toConnectsLifeline = lifelines.stream()
                      .filter(ll->ll.getParticipant().equals(to))
                      .anyMatch(ll->ll.overlaps(this));
  
              final int dir = from.getX_middle() < to.getX_middle()
                      ? 1
                      : -1;
              if(fromConnectsLifeline) {
                  x_from+= dir * LIFELINE_WIDTH / 2;
              }
              if(toConnectsLifeline) {
                  x_to-= dir * LIFELINE_WIDTH / 2;
              }
  
              x_left = Math.min(x_from, x_to);
  
              y_top = y_offset.getValue() + CONNECTION_MARGIN_V;
  
              textBlock = new TextBlock(label,
                      x_left,
                      y_top);
  
              var dim = textBlock.layout(g.getFontMetrics(),
                      CONNECTION_LABEL_PADDING_H,
                      CONNECTION_LABEL_PADDING_V,
                      CONNECTION_LABEL_LINEGAP,
                      Integer.MAX_VALUE);
  
              height = dim.height;
              y_bottom = y_top + height;
  
              y_offset.update(x->y_bottom);
          }
      }
  
      @Getter @RequiredArgsConstructor
      private static class Participant {
          final String label;
          int x_left;
          int x_middle;
          int x_right;
          int width;
  
          int y_top;
          int y_bottom;
          int height;
  
          TextBlock textBlock;
  
          void layout(final Graphics2D g, final IntReference x_offset) {
  
              x_left = x_offset.getValue();
              y_top = PARTICIPANT_MARGIN_V;
  
              textBlock = new TextBlock(label, x_left, y_top);
  
              var dim = textBlock.layout(g.getFontMetrics(),
                      PARTICIPANT_PADDING_H,
                      PARTICIPANT_PADDING_V,
                      PARTICIPANT_LINEGAP,
                      PARTICIPANT_MAX_CHAR_PER_LINE);
  
              width = dim.width;
              x_right = x_left + width;
              x_middle = (x_left + x_right) >> 1;
  
              height = dim.height;
              y_bottom = y_top + height;
  
              x_offset.update(x->x + width + PARTICIPANT_MARGIN_H);
          }
      }
  
      @Getter @RequiredArgsConstructor
      private static class Lifeline {
          final @NonNull Participant participant;
          final Connection startAt;
          Connection endAt;
  
          int x_left;
          int x_right;
          int width;
  
          int y_top;
          int y_bottom;
          int height;
  
          void layout(final Graphics2D g, final int min_y, final int max_y) {
  
              width = LIFELINE_WIDTH;
              x_left = participant.getX_middle() - LIFELINE_WIDTH / 2;
              x_right = x_left + width;
  
              y_top = startAt !=null
                      ? startAt.y_bottom
                      : min_y;
              y_bottom = endAt !=null
                      ? endAt.y_bottom
                      : max_y;
  
              height = y_bottom - y_top;
          }
  
          public boolean overlaps(final Connection connection) {
              var lowerBound = _Ints.Bound.inclusive(startAt != null
                      ? startAt.index
                      : -1);
              var upperBound = _Ints.Bound.inclusive(endAt !=null
                      ? endAt.index
                      : Integer.MAX_VALUE);
              return _Ints.Range.of(lowerBound, upperBound).contains(connection.index);
          }
      }
  
      public Dimension layout(final Graphics2D g) {
  
          PARTICIPANT_FONT.ifPresent(g::setFont);
  
          var x_offset = _Refs.intRef(PARTICIPANT_MARGIN_H);
          var y_offset = _Refs.intRef(0);
  
          participantsById.values().stream()
          .peek(p->p.layout(g, x_offset))
          .forEach(p->y_offset.update(x->Math.max(x, p.getHeight())));
  
          final int width = x_offset.getValue();
  
          y_offset.update(x->x + PARTICIPANT_MARGIN_V);
  
          final int y_offset_first_con = y_offset.getValue();
  
          CONNECTION_FONT.ifPresent(g::setFont);
  
          connections.stream()
          .forEach(c->c.layout(g, y_offset, lifelines));
  
          final int y_offset_last_con = y_offset.getValue();
  
          final int height = y_offset.update(x->x + 2*PARTICIPANT_MARGIN_V);
          this.size = new Dimension(width, height);
  
          lifelines.stream()
          .forEach(ll->ll.layout(g, y_offset_first_con - 3, y_offset_last_con + 3));
  
          return this.size;
      }
  
      public void render(final Graphics2D g) {
  
          _Graphics.enableTextAntialiasing(g);
  
          PARTICIPANT_FONT.ifPresent(g::setFont);
  
          participantsById.values().stream()
          .forEach(p->{
  
              // participant box
  
              g.setStroke(_Graphics.STROKE_DEFAULT);
  
              g.setColor(PARTICIPANT_BACKGROUND_COLOR);
              g.fillRect(p.getX_left(), p.getY_top(), p.getWidth(), p.getHeight());
  
              g.setColor(PARTICIPANT_BORDER_COLOR);
              g.drawRect(p.getX_left(), p.getY_top(), p.getWidth(), p.getHeight());
  
              // participant box label
  
              g.setColor(Color.black);
              p.getTextBlock().render(g);
  
              // participant vertical time line
  
              g.setColor(PARTICIPANT_BORDER_COLOR);
              g.setStroke(_Graphics.STROKE_DASHED);
              g.drawLine(p.getX_middle(), p.getY_bottom(), p.getX_middle(), size.height - PARTICIPANT_MARGIN_V);
          });
  
          g.setStroke(_Graphics.STROKE_DEFAULT);
  
          lifelines.stream()
          .forEach(ll->{
  
              // lifeline box
  
              g.setColor(LIFELINE_BACKGROUND_COLOR);
              g.fillRect(ll.getX_left(), ll.getY_top(), ll.getWidth(), ll.getHeight());
  
              g.setColor(PARTICIPANT_BORDER_COLOR);
              g.drawRect(ll.getX_left(), ll.getY_top(), ll.getWidth(), ll.getHeight());
          });
  
          CONNECTION_FONT.ifPresent(g::setFont);
  
          connections.stream()
          .forEach(c->{
  
              // connection arrow
  
              g.setColor(c.getArrowColor());
  
              g.setStroke(c.isDashedLine()
                      ? _Graphics.STROKE_DASHED
                      : _Graphics.STROKE_DEFAULT);
  
              _Graphics.arrowHorizontal(g,
                      c.getX_from(),
                      c.getX_to(),
                      c.getY_bottom());
  
              // connection label
  
              g.setColor(c.getLabelColor());
              c.getTextBlock().render(g);
  
          });
  
      }
  
  }
  