// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.

package com.intellij.history.integration.ui.views;

import com.intellij.history.core.revisions.Revision;
import com.intellij.history.integration.LocalHistoryBundle;
import com.intellij.history.integration.ui.models.HistoryDialogModel;
import com.intellij.history.integration.ui.models.RevisionItem;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.ui.ExpandableItemsHandler;
import com.intellij.ui.JBColor;
import com.intellij.ui.SeparatorWithText;
import com.intellij.ui.TableCell;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.table.JBTable;
import com.intellij.util.text.DateFormatUtil;
import com.intellij.util.ui.AbstractLayoutManager;
import com.intellij.util.ui.JBInsets;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.accessibility.AccessibleContextUtil;
import gnu.trove.THashMap;
import gnu.trove.THashSet;

import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.util.List;
import java.util.*;

public class RevisionsList {
  public static final int RECENT_PERIOD = 12;
  private final JBTable table;

  public RevisionsList(SelectionListener l) {
    table = new JBTable();
    table.setModel(new MyModel(Collections.emptyList(), Collections.emptyMap()));

    table.setTableHeader(null);
    table.setShowGrid(false);
    table.setRowMargin(0);
    table.getColumnModel().setColumnMargin(0);

    table.resetDefaultFocusTraversalKeys();

    table.setDefaultRenderer(Object.class, new MyCellRenderer(table));

    table.getEmptyText().setText(VcsBundle.message("history.empty"));

    addSelectionListener(l);
  }

  public JComponent getComponent() {
    return table;
  }

  private void addSelectionListener(SelectionListener listener) {
    final SelectionListener l = listener;

    table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
      private int mySelectedRow1 = 0;
      private int mySelectedRow2 = 0;
      private final SelectionListener mySelectionListener = l;

      @Override
      public void valueChanged(ListSelectionEvent e) {
        if (e.getValueIsAdjusting()) return;

        ListSelectionModel sm = table.getSelectionModel();
        mySelectedRow1 = sm.getMinSelectionIndex();
        mySelectedRow2 = sm.getMaxSelectionIndex();

        mySelectionListener.revisionsSelected(mySelectedRow1, mySelectedRow2);
      }
    });
  }

  public void updateData(HistoryDialogModel model) {
    Set<Long> sel = new THashSet<>();
    MyModel m = (MyModel)table.getModel();
    for (int i : table.getSelectedRows()) {
      if (i >= m.getRowCount()) continue;
      sel.add(m.getValueAt(i, 0).revision.getChangeSetId());
    }

    List<RevisionItem> newRevs = model.getRevisions();

    Date today = new Date();

    Map<RevisionItem, Period> periods = new THashMap<>();
    for (int i = 0; i < newRevs.size(); i++) {
      RevisionItem each = newRevs.get(i);
      boolean recent = today.getTime() - each.revision.getTimestamp() < 1000 * 60 * 60 * RECENT_PERIOD;
      if (recent) {
        if (i == 0) {
          periods.put(each, Period.RECENT);
        }
      }
      else {
        periods.put(each, periods.isEmpty() ? Period.OLD : Period.OLDER);
        break;
      }
    }

    table.setModel(new MyModel(newRevs, periods));

    for (int i = 0; i < newRevs.size(); i++) {
      RevisionItem r = newRevs.get(i);
      if (sel.contains(r.revision.getChangeSetId())) {
        table.getSelectionModel().addSelectionInterval(i, i);
      }
    }
    if (table.getSelectionModel().isSelectionEmpty()) {
      table.getSelectionModel().addSelectionInterval(0, 0);
    }
  }

  public interface SelectionListener {
    void revisionsSelected(int first, int last);
  }

  private enum Period {
    RECENT(LocalHistoryBundle.message("revisions.table.period.recent", RECENT_PERIOD)),
    OLDER(LocalHistoryBundle.message("revisions.table.period.older")),
    OLD(LocalHistoryBundle.message("revisions.table.period.old"));

    private final String myDisplayString;

    Period(String displayString) {
      myDisplayString = displayString;
    }

    public String getDisplayString() {
      return myDisplayString;
    }
  }

  public static class MyModel extends AbstractTableModel {
    private final List<? extends RevisionItem> myRevisions;
    private final Map<RevisionItem, Period> myPeriods;

    public MyModel(List<? extends RevisionItem> revisions, Map<RevisionItem, Period> periods) {
      myRevisions = revisions;
      myPeriods = periods;
    }

    @Override
    public int getColumnCount() {
      return 1;
    }

    @Override
    public int getRowCount() {
      return myRevisions.size();
    }

    @Override
    public RevisionItem getValueAt(int rowIndex, int columnIndex) {
      return myRevisions.get(rowIndex);
    }

    public Period getPeriod(RevisionItem r) {
      return myPeriods.get(r);
    }
  }

  public static class MyCellRenderer implements TableCellRenderer {
    private static final Color USER_LABEL_COLOR = new JBColor(new Color(230, 230, 250), new Color(89, 96, 74));
    private static final Insets BORDER_INSETS = new Insets(2, 5, 2, 5);

    private final DefaultTableCellRenderer myTemplate = new DefaultTableCellRenderer();

    private final MyWrapperPanel myWrapperPanel = new MyWrapperPanel();
    private final JPanel myItemPanel = new JPanel();

    private final MyBorder myBorder = new MyBorder(BORDER_INSETS);
    private final SeparatorWithText myPeriodLabel = new SeparatorWithText();

    private final JBLabel myDateLabel = new JBLabel();

    private final JBLabel myFilesCountLabel = new JBLabel();
    private final JBLabel myTitleLabel = new JBLabel();

    private final JPanel myLabelPanel = new JPanel();
    private final MyLabelContainer myLabelContainer = new MyLabelContainer();
    private final JBLabel myLabelLabel = new JBLabel();

    private final ExpandableItemsHandler<TableCell> myToolTipHandler;

    public MyCellRenderer(JBTable table) {
      myToolTipHandler = table.getExpandableItemsHandler();
      JPanel headersPanel = new JPanel(new BorderLayout());
      headersPanel.setOpaque(false);
      headersPanel.add(myPeriodLabel, BorderLayout.NORTH);
      headersPanel.add(myLabelPanel, BorderLayout.CENTER);

      myLabelContainer.add(myLabelLabel);

      myLabelPanel.setLayout(new AbstractLayoutManager() {
        @Override
        public Dimension preferredLayoutSize(Container parent) {
          Dimension size = myLabelContainer.getPreferredSize();
          JBInsets.addTo(size, parent.getInsets());
          return size;
        }

        @Override
        public void layoutContainer(Container parent) {
          Insets i = parent.getInsets();

          int pw = parent.getWidth() - i.left - i.right;

          Dimension pref = myLabelContainer.getPreferredSize();
          int w = Math.min(pw, pref.width);
          int h = pref.height;

          int x = i.left + pw - w;
          int y = i.top;

          myLabelContainer.setBounds(x, y, w, h);
        }
      });
      myLabelPanel.setOpaque(false);
      myLabelPanel.add(myLabelContainer);

      final JPanel layoutPanel = new JPanel(new BorderLayout());
      layoutPanel.setOpaque(false);

      layoutPanel.add(headersPanel, BorderLayout.NORTH);
      layoutPanel.add(myItemPanel, BorderLayout.CENTER);

      myWrapperPanel.setLayout(new AbstractLayoutManager() {
        @Override
        public Dimension preferredLayoutSize(Container parent) {
          return layoutPanel.getPreferredSize();
        }

        @Override
        public void layoutContainer(Container parent) {
          Dimension size = parent.getSize();
          Insets i = parent.getInsets();
          Dimension pref = layoutPanel.getPreferredSize();
          layoutPanel.setBounds(i.left, i.top, size.width - i.left - i.right, pref.height);
        }
      });
      myWrapperPanel.add(layoutPanel);

      myItemPanel.setBorder(myBorder);
      myItemPanel.setLayout(new BorderLayout());
      JPanel north = new JPanel(new BorderLayout());
      north.setOpaque(false);
      north.add(myDateLabel, BorderLayout.WEST);
      north.add(myFilesCountLabel, BorderLayout.EAST);

      JPanel south = new JPanel(new BorderLayout());
      south.add(myTitleLabel, BorderLayout.CENTER);
      south.setOpaque(false);
      myItemPanel.add(north, BorderLayout.NORTH);
      myItemPanel.add(south, BorderLayout.SOUTH);

      myLabelLabel.setBorder(new EmptyBorder(0, 5, 1, 5));
      myLabelPanel.setBorder(new MyBorder(new Insets(4, 20, 3, 1)));

      myWrapperPanel.setOpaque(false);
      myItemPanel.setOpaque(true);

      myWrapperPanel.setBackground(table.getBackground());

      myDateLabel.setComponentStyle(UIUtil.ComponentStyle.SMALL);
      myFilesCountLabel.setComponentStyle(UIUtil.ComponentStyle.SMALL);
      myLabelLabel.setComponentStyle(UIUtil.ComponentStyle.REGULAR);
      myTitleLabel.setComponentStyle(UIUtil.ComponentStyle.REGULAR);
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
      if (value == null) return myWrapperPanel; // null erroneously comes from JPanel.getAccessibleChild

      RevisionItem r = (RevisionItem)value;
      LabelsAndColor labelsAndColor = getLabelsAndColor(r);

      final Period p = ((MyModel)table.getModel()).getPeriod(r);
      if (p == null) {
        myPeriodLabel.setVisible(false);
      }
      else {
        myPeriodLabel.setVisible(true);
        myPeriodLabel.setCaption(p.getDisplayString());
      }

      myBorder.set(row == table.getModel().getRowCount() - 1);

      myDateLabel.setText(ensureString(DateFormatUtil.formatPrettyDateTime(r.revision.getTimestamp())));
      myFilesCountLabel.setText(ensureString(labelsAndColor.filesCount));

      myTitleLabel.setFont(myTitleLabel.getFont().deriveFont(labelsAndColor.isNamed ? Font.BOLD : Font.PLAIN));
      myTitleLabel.setText(ensureString(labelsAndColor.title));

      JComponent orig = (JComponent)myTemplate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);

      if (labelsAndColor.label == null) {
        myLabelPanel.setVisible(false);
      }
      else {
        myLabelPanel.setVisible(true);
        myLabelLabel.setBackground(labelsAndColor.label.second);
        myLabelContainer.set(labelsAndColor.label.second);
        myLabelLabel.setText(ensureString(labelsAndColor.label.first));
      }

      Color fg = orig.getForeground();
      Color bg = isSelected && !table.isFocusOwner() ? UIUtil.getListUnfocusedSelectionBackground() : orig.getBackground();

      myDateLabel.setForeground(isSelected ? fg : JBColor.GRAY);
      myFilesCountLabel.setForeground(myDateLabel.getForeground());
      myTitleLabel.setForeground(isSelected || labelsAndColor.isNamed ? fg : JBColor.DARK_GRAY);

      myItemPanel.setBackground(bg);

      myWrapperPanel.doLayout();
      int height = myWrapperPanel.getPreferredSize().height;
      //table.setRowHeight causes extra repaint of the table, so we try to avoid it.
      if (table.getRowHeight(row) != height && height > 0) {
        table.setRowHeight(row, height);
      }

      return myWrapperPanel;
    }

    private String ensureString(String s) {
      return StringUtil.isEmpty(s) ? " " : s;
    }

    private LabelsAndColor getLabelsAndColor(RevisionItem item) {
      Revision r = item.revision;

      final Pair<List<String>, Integer> affected = r.getAffectedFileNames();

      String title = r.getChangeSetName();
      boolean named = title != null;
      if (title == null) {
        title = StringUtil.join(affected.first, ", ");
        if (affected.first.size() < affected.second) title += "...";
      }

      String filesCount = StringUtil.pluralize(LocalHistoryBundle.message("revisions.table.filesCount", affected.second), affected.second);

      Pair<String, Color> label = null;
      if (!item.labels.isEmpty()) {
        Revision first = item.labels.getFirst();
        label = Pair.create(first.getLabel(), first.getLabelColor() == -1 ? USER_LABEL_COLOR : new Color(first.getLabelColor()));
      }

      return new LabelsAndColor(named, title, filesCount, label);
    }

    /**
     * Given each item in the list of revisions contains multiple strings,
     * we customize the containing panel to expose an accessible name
     * combining all these strings so that screen readers announce these
     * strings as the active list item changes.
     */
    private class MyWrapperPanel extends JPanel {
      @Override
      public AccessibleContext getAccessibleContext() {
        if (accessibleContext == null) {
          accessibleContext = new AccessibleMyWrapperPanel();
        }
        return accessibleContext;
      }

      protected class AccessibleMyWrapperPanel extends AccessibleJPanel {
        @Override
        public AccessibleRole getAccessibleRole() {
          return AccessibleRole.LABEL;
        }

        @Override
        public String getAccessibleName() {
          if (myPeriodLabel.isVisible()) {
            return AccessibleContextUtil.getCombinedName(", ", myPeriodLabel, myTitleLabel, myFilesCountLabel, myDateLabel);
          } else {
            return AccessibleContextUtil.getCombinedName(", ", myTitleLabel, myFilesCountLabel, myDateLabel);
          }
        }
      }
    }

    private static class LabelsAndColor {
      final boolean isNamed;
      final String title;
      final String filesCount;
      final Pair<String, Color> label;

      private LabelsAndColor(boolean isNamed, String title, String filesCount, Pair<String, Color> label) {
        this.isNamed = isNamed;
        this.title = title;
        this.filesCount = filesCount;
        this.label = label;
      }
    }

    private static class MyBorder extends EmptyBorder {
      private boolean isLast;

      private MyBorder(Insets insets) {
        super(insets);
      }

      public void set(boolean isLast) {
        this.isLast = isLast;
      }

      @Override
      public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
        Graphics2D g2d = (Graphics2D)g.create();
        g2d.setColor(UIUtil.getBorderColor());
        g2d.setStroke(new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, new float[]{1}, 1));
        g2d.drawLine(x, y, x + width, y);
        if (isLast) {
          g2d.drawLine(x, y + height - 1, x + width, y + height - 1);
        }
        g2d.dispose();
      }
    }

    private static class MyLabelContainer extends JPanel {
      private MyLabelContainer() {
        super(new BorderLayout());
      }

      public void set(Color c) {
        setBackground(c);
      }

      @Override
      public Dimension getMinimumSize() {
        return super.getMinimumSize();
      }

      @Override
      protected void paintComponent(Graphics g) {
        Graphics2D g2d = (Graphics2D)g.create();
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        g2d.setColor(getBackground());
        g2d.fillRoundRect(0, 0, getWidth(), getHeight(), getHeight() - 2, getHeight() - 2);

        g2d.setColor(getBackground().darker());
        g2d.drawRoundRect(0, 0, getWidth() - 1, getHeight() - 1, getHeight() - 2, getHeight() - 2);
        g2d.dispose();
      }
    }
  }
}
