/*
 * 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.geode.sequence;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.Popup;
import javax.swing.PopupFactory;

/**
 * Created by IntelliJ IDEA. User: dsmith Date: Oct 29, 2010 Time: 4:18:40 PM To change this
 * template use File | Settings | File Templates.
 */
public class SequenceDiagram extends JPanel {

  private static final Color HIGHLIGHT_COLOR = Color.RED;

  private final List<String> lineNames;
  private final Map<String, List<String>> shortLineNames;
  private final SortedMap<Comparable, SubDiagram> subDiagrams =
      new TreeMap<>();
  private final StateColorMap colorMap = new StateColorMap();
  private final long minTime;
  private final long maxTime;

  private static final int PADDING_BETWEEN_LINES = 100;
  private static final int Y_PADDING = 20;
  private static final int STATE_WIDTH = 20;
  private static final int LINE_LABEL_BOUNDARY = 5;
  private static final int AXIS_SIZE = 35;

  private int lineStep;
  private int lineWidth;
  private Popup mouseover;
  private LifelineState mouseoverState;
  private LifelineState selectedState;

  public SequenceDiagram(long minTime, long maxTime, List<String> lineNames,
      LineMapper lineMapper) {
    this.lineNames = lineNames;
    shortLineNames = parseShortNames(lineNames, lineMapper);
    this.minTime = minTime;
    this.maxTime = maxTime;
    int width = getInitialWidth();
    int height = 500;
    super.setPreferredSize(new Dimension(width, height));
    resizeMe(width, height);
    addComponentListener(new ComponentAdapter() {
      @Override
      public void componentResized(ComponentEvent e) {
        Component source = (Component) e.getSource();
        resizeMe(source.getWidth(), source.getHeight());
      }
    });
    setBackground(Color.WHITE);
  }

  private Map<String, List<String>> parseShortNames(List<String> lineNames, LineMapper lineMapper) {
    Map<String, List<String>> shortNames =
        new LinkedHashMap<>(lineNames.size());
    for (String name : lineNames) {
      String shortName = lineMapper.getShortNameForLine(name);
      List<String> list = shortNames.get(shortName);
      if (list == null) {
        list = new ArrayList<>();
        shortNames.put(shortName, list);
      }
      list.add(name);
    }

    return shortNames;
  }

  public List<Comparable> getSubDiagramsNames() {
    return new ArrayList<>(subDiagrams.keySet());

  }

  public void addSubDiagram(Comparable name, Map<String, Lifeline> lines, List<Arrow> arrows) {
    subDiagrams.put(name, new SubDiagram(name, lines, arrows));
  }

  public void removeSubDiagram(Comparable name) {
    subDiagrams.remove(name);
  }

  private int getInitialWidth() {
    return (shortLineNames.size()) * PADDING_BETWEEN_LINES;
  }

  public void resizeMe(int width, int height) {
    setPreferredSize(new Dimension(width, height));
    float xZoom = width / (float) getInitialWidth();


    long elapsedTime = maxTime - minTime;
    double yScale = height / (double) elapsedTime;
    // long yBase = (long) (minTime - ((long) Y_PADDING / yScale));
    long yBase = minTime;
    lineStep = (int) (PADDING_BETWEEN_LINES * xZoom);
    lineWidth = (int) (STATE_WIDTH * xZoom);

    if (subDiagrams.size() <= 0) {
      return;
    }

    int sublineWidth = lineWidth / subDiagrams.size();
    int sublineIndex = 0;
    for (SubDiagram diagram : subDiagrams.values()) {
      int lineIndex = 0;
      for (List<String> fullNames : shortLineNames.values()) {
        for (String name : fullNames) {
          Lifeline line = diagram.lifelines.get(name);
          if (line != null) {
            int lineX = lineIndex * lineStep + sublineIndex * sublineWidth;
            line.resize(lineX, sublineWidth, yBase, yScale);
          }
        }
        lineIndex++;
      }
      sublineIndex++;
    }
  }

  public void showPopupText(int x, int y, int xOnScreen, int yOnScreen) {
    LifelineState state = getStateAt(x, y);
    if (state == mouseoverState) {
      return;
    }
    if (mouseover != null) {
      mouseover.hide();
    }
    if (state == null) {
      mouseover = null;
      mouseoverState = null;
    } else {
      Component popupContents = state.getPopup();
      mouseoverState = state;
      mouseover =
          PopupFactory.getSharedInstance().getPopup(this, popupContents, xOnScreen + 20, yOnScreen);
      mouseover.show();
    }
  }

  public void selectState(int x, int y) {
    LifelineState state = getStateAt(x, y);
    if (state == selectedState) {
      return;
    }

    if (selectedState != null) {
      fireRepaintOfDependencies(selectedState);
    }

    selectedState = state;

    if (state != null) {
      fireRepaintOfDependencies(state);
    }
  }

  private LifelineState getStateAt(int x, int y) {
    // TODO - this needs some
    // serious optimization to go straight to the right state
    // I think we need a tree map of of lines, keyed by x offset
    // and a keymap of states keyed by y offset.
    // That could make painting faster as well.
    List<String> reverseList = new ArrayList<>(lineNames);
    Collections.reverse(reverseList);
    for (SubDiagram diagram : subDiagrams.values()) {
      for (String name : reverseList) {
        Lifeline line = diagram.lifelines.get(name);
        if (line != null) {
          if (line.getX() < x && line.getX() + line.getWidth() > x) {
            LifelineState state = line.getStateAt(y);
            if (state != null) {
              return state;
            }
          }
        }
      }
    }
    return null;
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g.create();
    // TODO - we should clip these to the visible lines
    for (SubDiagram subDiagram : subDiagrams.values()) {
      subDiagram.paintStates(g2, colorMap);
    }

    for (SubDiagram subDiagram : subDiagrams.values()) {
      subDiagram.paintArrows(g2, colorMap);
    }
    paintHighlightedComponents(g2, selectedState, new HashSet<>());
  }

  private void fireRepaintOfDependencies(LifelineState state) {
    // TODO - it might be more efficient to repaint just the changed
    // areas, but right now this will do.
    repaint();
  }

  private void paintHighlightedComponents(Graphics2D g2, LifelineState endingState,
      Set<LifelineState> visited) {
    if (!visited.add(endingState)) {
      // Prevent cycles
      return;
    }
    g2.setColor(HIGHLIGHT_COLOR);
    if (endingState != null) {
      endingState.highlight(g2);

      for (Arrow arrow : endingState.getInboundArrows()) {
        arrow.paint(g2);
        paintHighlightedComponents(g2, arrow.getStartingState(), visited);
      }
    }


  }

  public long getMinTime() {
    return minTime;
  }

  public long getMaxTime() {
    return maxTime;
  }

  public JComponent createMemberAxis() {
    return new MemberAxis();
  }

  private class MemberAxis extends JComponent {
    public MemberAxis() {
      setPreferredSize(new Dimension(getWidth(), AXIS_SIZE));
      SequenceDiagram.this.addComponentListener(new ComponentAdapter() {
        @Override
        public void componentResized(ComponentEvent e) {
          int newWidth = e.getComponent().getWidth();
          setPreferredSize(new Dimension(newWidth, AXIS_SIZE));
          revalidate();
        }
      });
    }

    @Override
    protected void paintComponent(Graphics g) {
      super.paintComponent(g);
      Rectangle bounds = g.getClipBounds();
      int index = 0;
      for (String name : shortLineNames.keySet()) {
        int nameWidth = g.getFontMetrics().stringWidth(name);
        int lineX = lineStep * index;
        index++;
        if (bounds.getMaxX() < lineX || bounds.getMinX() > lineX + nameWidth) {
          continue;
        }
        g.setClip(lineX + LINE_LABEL_BOUNDARY, 0, lineStep - +LINE_LABEL_BOUNDARY * 2, getHeight());
        g.drawString(name, lineX + LINE_LABEL_BOUNDARY, AXIS_SIZE / 3);
        g.setClip(null);
      }
    }
  }

  public static class SubDiagram {
    private final Map<String, Lifeline> lifelines;
    private final List<Arrow> arrows;
    private final Comparable name;

    public SubDiagram(Comparable name, Map<String, Lifeline> lines, List<Arrow> arrows) {
      this.name = name;
      lifelines = lines;
      this.arrows = arrows;
    }

    public void paintStates(Graphics2D g2, StateColorMap colorMap) {
      for (Lifeline line : lifelines.values()) {
        line.paint(g2, colorMap);
      }
    }

    public void paintArrows(Graphics2D g2, StateColorMap colorMap) {
      Color lineColor = colorMap.getColor(name);
      g2.setColor(lineColor);
      for (Arrow arrow : arrows) {
        arrow.paint(g2);
      }
    }
  }
}
