/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org

  Copyright (c) 2013-15 The Processing Foundation
  Copyright (c) 2011-12 Ben Fry and Casey Reas

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License version 2
  as published by the Free Software Foundation.

  This program 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 General Public License for more details.

  You should have received a copy of the GNU General Public License along
  with this program; if not, write to the Free Software Foundation, Inc.
  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
package processing.app.contrib;

import java.awt.*;
import java.util.List;
import java.util.*;
import java.util.Map.Entry;

import javax.swing.*;
import javax.swing.RowSorter.SortKey;
import javax.swing.border.Border;
import javax.swing.event.*;
import javax.swing.table.*;

import processing.app.Base;
import processing.app.Platform;
import processing.app.ui.Toolkit;


// The "Scrollable" implementation and its methods here take care of preventing
// the scrolling area from running exceptionally slowly. Not sure why they're
// necessary in the first place, however; seems like odd behavior.
// It also allows the description text in the panels to wrap properly.

public class ListPanel extends JPanel
implements Scrollable, ContributionListing.ChangeListener {
  ContributionTab contributionTab;
  TreeMap<Contribution, DetailPanel> panelByContribution = new TreeMap<Contribution, DetailPanel>(ContributionListing.COMPARATOR);
  Set<Contribution> visibleContributions = new TreeSet<Contribution>(ContributionListing.COMPARATOR);

  private DetailPanel selectedPanel;
  protected Contribution.Filter filter;
  protected ContributionListing contribListing = ContributionListing.getInstance();
  protected JTable table;
  DefaultTableModel model;
  JScrollPane scrollPane;

  static Icon upToDateIcon;
  static Icon updateAvailableIcon;
  static Icon incompatibleIcon;
  static Icon foundationIcon;
  static Icon downloadingIcon;

  // Should this be in theme.txt? Of course! Is it? No.
  static final Color HEADER_BGCOLOR = new Color(0xffEBEBEB);


  public ListPanel() {
    if (upToDateIcon == null) {
      upToDateIcon = Toolkit.getLibIconX("manager/up-to-date");
      updateAvailableIcon = Toolkit.getLibIconX("manager/update-available");
      incompatibleIcon = Toolkit.getLibIconX("manager/incompatible");
      foundationIcon = Toolkit.getLibIconX("icons/foundation", 16);
      downloadingIcon = Toolkit.getLibIconX("manager/downloading");
    }
  }


  public ListPanel(final ContributionTab contributionTab,
                   Contribution.Filter filter) {
    this.contributionTab = contributionTab;
    this.filter = filter;

    setLayout(new GridBagLayout());
    setOpaque(true);
    setBackground(Color.WHITE);
    model = new ContribTableModel();
    table = new JTable(model) {
      @Override
      public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
        Component c = super.prepareRenderer(renderer, row, column);
        if (isRowSelected(row)) {
          c.setBackground(new Color(0xe0fffd));
        } else {
          c.setBackground(Color.white);
        }
        return c;
      }
    };

    // There is a space before Status
    String[] colName = { " Status", "Name", "Author" };
    model.setColumnIdentifiers(colName);
    scrollPane = new JScrollPane(table);
    scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
    scrollPane.setBorder(BorderFactory.createEmptyBorder());
    table.setFillsViewportHeight(true);
    table.setDefaultRenderer(Contribution.class, new ContribStatusRenderer());
    table.setFont(ManagerFrame.NORMAL_PLAIN);
    table.setRowHeight(Toolkit.zoom(28));
    table.setRowMargin(Toolkit.zoom(6));
    table.getColumnModel().setColumnMargin(0);
    table.getColumnModel().getColumn(0).setMaxWidth(ManagerFrame.STATUS_WIDTH);
    table.getColumnModel().getColumn(2).setMinWidth(ManagerFrame.AUTHOR_WIDTH);
    table.getColumnModel().getColumn(2).setMaxWidth(ManagerFrame.AUTHOR_WIDTH);
    table.setShowGrid(false);
    table.setColumnSelectionAllowed(false);
    table.setCellSelectionEnabled(false);
    table.setAutoCreateColumnsFromModel(true);
    table.setAutoCreateRowSorter(false);
    table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

    table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
        public void valueChanged(ListSelectionEvent event) {
          //TODO this executes 2 times when clicked and 1 time when traversed using arrow keys
          //Ideally this should always be true but while clearing the table something fishy is going on
          if (table.getSelectedRow() != -1) {
            setSelectedPanel(panelByContribution.get(table.getValueAt(table
              .getSelectedRow(), 0)));
            // Preventing the focus to move out of filterField after typing every character
            if (!contributionTab.filterField.hasFocus()) {
              table.requestFocusInWindow();
            }
          }
        }
      });

    TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
    table.setRowSorter(sorter);
    sorter.setComparator(1, ContributionListing.COMPARATOR);
    sorter.setComparator(2, new Comparator<Contribution>() {

      @Override
      public int compare(Contribution o1, Contribution o2) {
        return getAuthorNameWithoutMarkup(o1.getAuthorList())
          .compareTo(getAuthorNameWithoutMarkup(o2.getAuthorList()));
      }
    });
    sorter.setComparator(0, new Comparator<Contribution>() {

      @Override
      public int compare(Contribution o1, Contribution o2) {
        int pos1 = 0;
        if (o1.isInstalled()) {
          pos1 = 1;
          if (contribListing.hasUpdates(o1)) {
            pos1 = 2;
          }
          if (!o1.isCompatible(Base.getRevision())) {
            pos1 = 3;
          }
        } else {
          pos1 = 4;
        }
        int pos2 = 0;
        if (o2.isInstalled()) {
          pos2 = 1;
          if (contribListing.hasUpdates(o2)) {
            pos2 = 2;
          }
          if (!o2.isCompatible(Base.getRevision())) {
            pos2 = 3;
          }
        } else {
          pos2 = 4;
        }
        return pos1 - pos2;
      }
    });
    table.getTableHeader().setDefaultRenderer(new ContribHeaderRenderer());

    GroupLayout layout = new GroupLayout(this);
    layout.setHorizontalGroup(layout.createParallelGroup().addComponent(scrollPane));
    layout.setVerticalGroup(layout.createSequentialGroup().addComponent(scrollPane));

    this.setLayout(layout);
    table.setVisible(true);
  }


  class ContribHeaderRenderer extends DefaultTableCellRenderer {

    public ContribHeaderRenderer() {
      setHorizontalTextPosition(LEFT);
      setOpaque(true);
    }

    /**
     * Returns the default table header cell renderer.
     * <P>
     * If the column is sorted, the appropriate icon is retrieved from the
     * current Look and Feel, and a border appropriate to a table header cell
     * is applied.
     * <P>
     * Subclasses may override this method to provide custom content or
     * formatting.
     *
     * @param table the <code>JTable</code>.
     * @param value the value to assign to the header cell
     * @param isSelected This parameter is ignored.
     * @param hasFocus This parameter is ignored.
     * @param row This parameter is ignored.
     * @param column the column of the header cell to render
     * @return the default table header cell renderer
     */
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
            boolean isSelected, boolean hasFocus, int row, int column) {
      super.getTableCellRendererComponent(table, value,
              isSelected, hasFocus, row, column);

      JTableHeader tableHeader = table.getTableHeader();
      if (tableHeader != null) {
        setForeground(tableHeader.getForeground());
      }
      setFont(ManagerFrame.SMALL_PLAIN);
      setIcon(getSortIcon(table, column));
      setBackground(HEADER_BGCOLOR);
//      if (column % 2 == 0) {
//        setBackground(new Color(0xdfdfdf));
//      } else {
//        setBackground(new Color(0xebebeb));
//      }
      setBorder(null);
      return this;
    }

    /**
     * Overloaded to return an icon suitable to the primary sorted column, or null if
     * the column is not the primary sort key.
     *
     * @param table the <code>JTable</code>.
     * @param column the column index.
     * @return the sort icon, or null if the column is unsorted.
     */
    protected Icon getSortIcon(JTable table, int column) {
      SortKey sortKey = getSortKey(table, column);
      if (sortKey != null && table.convertColumnIndexToView(sortKey.getColumn()) == column) {
        switch (sortKey.getSortOrder()) {
          case ASCENDING:
            return UIManager.getIcon("Table.ascendingSortIcon");
          case DESCENDING:
            return UIManager.getIcon("Table.descendingSortIcon");
        }
      }
      return null;
    }

    /**
     * Returns the current sort key, or null if the column is unsorted.
     *
     * @param table the table
     * @param column the column index
     * @return the SortKey, or null if the column is unsorted
     */
    protected SortKey getSortKey(JTable table, int column) {
      RowSorter rowSorter = table.getRowSorter();
      if (rowSorter == null) {
        return null;
      }

      List sortedColumns = rowSorter.getSortKeys();
      if (sortedColumns.size() > 0) {
        return (SortKey) sortedColumns.get(0);
      }
      return null;
    }
  }


  private class ContribStatusRenderer extends DefaultTableCellRenderer {

    @Override
    public void setVerticalAlignment(int alignment) {
      super.setVerticalAlignment(SwingConstants.CENTER);
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
                                                   boolean isSelected,
                                                   boolean hasFocus, int row,
                                                   int column) {
      Contribution contribution = (Contribution) value;
      JLabel label = new JLabel();
      if (value == null) {
        // Working on https://github.com/processing/processing/issues/3667
        //System.err.println("null value seen in getTableCellRendererComponent()");
        // TODO this is now working, but the underlying issue is not fixed
        return label;
      }
      if (column == 0) {
        Icon icon = null;
        label.setFont(ManagerFrame.NORMAL_PLAIN);
        if (contribution.isInstalled()) {
          icon = upToDateIcon;
          if (contribListing.hasUpdates(contribution)) {
            icon = updateAvailableIcon;
          }
          if (!contribution.isCompatible(Base.getRevision())) {
            icon = incompatibleIcon;
          }
        }
        if ((panelByContribution.get(contribution)).updateInProgress ||
          (panelByContribution.get(contribution)).installInProgress) {
          // Display "Loading icon" if download/install in progress
          label.setIcon(downloadingIcon);
        } else {
          label.setIcon(icon);
        }
        label.setHorizontalAlignment(SwingConstants.CENTER);
        if (isSelected) {
          label.setBackground(new Color(0xe0fffd));
        }
        label.setOpaque(true);
//        return table.getDefaultRenderer(Icon.class).getTableCellRendererComponent(table, icon, isSelected, false, row, column);

      } else if (column == 1) {
        // Generating ellipses based on fontMetrics
        final Font boldFont = ManagerFrame.NORMAL_BOLD;
        String fontFace = "<font face=\"" + boldFont.getName() + "\">";
        FontMetrics fontMetrics = table.getFontMetrics(boldFont); //table.getFont());
        int colSize = table.getColumnModel().getColumn(1).getWidth();
        String sentence = contribution.getSentence();
        //int currentWidth = table.getFontMetrics(table.getFont().deriveFont(Font.BOLD)).stringWidth(contribution.getName() + " | ");
        int currentWidth = table.getFontMetrics(boldFont).stringWidth(contribution.getName() + " | ");
        int ellipsesWidth = fontMetrics.stringWidth("...");
        //String name = "<html><body><b>" + contribution.getName();
        String name = "<html><body>" + fontFace + contribution.getName();
        if (sentence == null) {
          label.setText(name + "</font></body></html>");
        } else {
          sentence = " | </font>" + sentence;
          currentWidth += ellipsesWidth;
          int i = 0;
          for (i = 0; i < sentence.length(); i++) {
            currentWidth += fontMetrics.charWidth(sentence.charAt(i));
            if (currentWidth >= colSize) {
              break;
            }
          }
          // Adding ellipses only if text doesn't fits into the column
          if(i != sentence.length()){
            label.setText(name + sentence.substring(0, i) + "...</body></html>");
          }else {
            label.setText(name + sentence + "</body></html>");
          }
        }
        if (!contribution.isCompatible(Base.getRevision())) {
          label.setForeground(Color.LIGHT_GRAY);
        }
        if (table.isRowSelected(row)) {
          label.setBackground(new Color(0xe0fffd));
        }
        label.setFont(ManagerFrame.NORMAL_PLAIN);
        label.setOpaque(true);
      } else {
        if (contribution.isSpecial()) {
          label = new JLabel(foundationIcon);
        } else {
          label = new JLabel();
        }
        String authorList = contribution.getAuthorList();
        String name = getAuthorNameWithoutMarkup(authorList);
        label.setText(name);
        label.setHorizontalAlignment(SwingConstants.LEFT);
        if(!contribution.isCompatible(Base.getRevision())){
          label.setForeground(Color.LIGHT_GRAY);
        }else{
          label.setForeground(Color.BLACK);
        }
        if (table.isRowSelected(row)) {
          label.setBackground(new Color(0xe0fffd));
        }
        label.setFont(ManagerFrame.NORMAL_BOLD);
        label.setOpaque(true);
      }
      return label;
    }
  }


  static private class ContribTableModel extends DefaultTableModel {
    @Override
    public boolean isCellEditable(int row, int column) {
      return false;
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
      return Contribution.class;
    }
  }


  String getAuthorNameWithoutMarkup(String authorList) {
    StringBuilder name = new StringBuilder("");
    if (authorList != null) {
      for (int i = 0; i < authorList.length(); i++) {

        if (authorList.charAt(i) == '[' || authorList.charAt(i) == ']') {
          continue;
        }
        if (authorList.charAt(i) == '(') {
          i++;
          while (authorList.charAt(i) != ')') {
            i++;
          }
        } else {
          name.append(authorList.charAt(i));
        }
      }
    }
    return name.toString();
  }

  // Thread: EDT
  void updatePanelOrdering(Set<Contribution> contributionsSet) {
    model.getDataVector().removeAllElements();
    int rowCount = 0;
    for (Contribution entry : contributionsSet) {
      model.addRow(new Object[]{entry, entry, entry});
      if (selectedPanel != null &&
          entry.getName().equals(selectedPanel.getContrib().getName())) {
        table.setRowSelectionInterval(rowCount, rowCount);
      }
      rowCount++;
    }
    model.fireTableDataChanged();
  }


  // Thread: EDT
  public void contributionAdded(final Contribution contribution) {
    if (filter.matches(contribution)) {
      if (!panelByContribution.containsKey(contribution)) {
        DetailPanel newPanel =
          new DetailPanel(ListPanel.this);
        panelByContribution.put(contribution, newPanel);
        visibleContributions.add(contribution);
        newPanel.setContribution(contribution);
        add(newPanel);
        updatePanelOrdering(visibleContributions);
        updateColors();  // XXX this is the place
      }
    }
  }


  // Thread: EDT
  public void contributionRemoved(final Contribution contribution) {
    if (filter.matches(contribution)) {
      DetailPanel panel = panelByContribution.get(contribution);
      if (panel != null) {
        remove(panel);
        panelByContribution.remove(contribution);
      }
      visibleContributions.remove(contribution);
      updatePanelOrdering(visibleContributions);
      updateColors();
      updateUI();
    }
  }


  // Thread: EDT
  public void contributionChanged(final Contribution oldContrib,
                                  final Contribution newContrib) {
    if (filter.matches(oldContrib) || filter.matches(newContrib)) {
      DetailPanel panel = panelByContribution.get(oldContrib);
      if (panel == null) {
        contributionAdded(newContrib);
      } else {
        panelByContribution.remove(oldContrib);
        panel.setContribution(newContrib);
        panelByContribution.put(newContrib, panel);
      }
      if (visibleContributions.contains(oldContrib)) {
        visibleContributions.remove(oldContrib);
        visibleContributions.add(newContrib);
      }
      updatePanelOrdering(visibleContributions);
    }
  }


  // Thread: EDT
  public void filterLibraries(List<Contribution> filteredContributions) {
    visibleContributions.clear();
    for (Contribution contribution : panelByContribution.keySet()) {
      if (contribution.getType() == contributionTab.contribType) {
        // contains() uses equals() and there can be multiple instances,
        // so Contribution.equals() has to be overridden
        if (filteredContributions.contains(contribution)) {
          if (panelByContribution.keySet().contains(contribution)) {
            visibleContributions.add(contribution);
          }
        }
      }
    }
    // TODO: Make the following loop work for optimization
//  for (Contribution contribution : filteredContributions) {
//    if (contribution.getType() == contributionTab.contribType) {
//      if(panelByContribution.keySet().contains(contribution)){
//       visibleContributions.add(contribution);
//      }
//    }
//  }
    updatePanelOrdering(visibleContributions);
  }


  // Thread: EDT
  protected void setSelectedPanel(DetailPanel contributionPanel) {
    contributionTab.updateStatusPanel(contributionPanel);

    if (selectedPanel == contributionPanel) {
      selectedPanel.setSelected(true);

    } else {
      DetailPanel lastSelected = selectedPanel;
      selectedPanel = contributionPanel;

      if (lastSelected != null) {
        lastSelected.setSelected(false);
      }
      contributionPanel.setSelected(true);

      updateColors();
      requestFocusInWindow();
    }
  }


  protected DetailPanel getSelectedPanel() {
    return selectedPanel;
  }


  // Thread: EDT
  /**
   * Updates the colors of all library panels that are visible.
   */
  protected void updateColors() {
    int count = 0;
    for (Entry<Contribution, DetailPanel> entry : panelByContribution.entrySet()) {
      DetailPanel panel = entry.getValue();

      if (panel.isVisible() && panel.isSelected()) {
        panel.setBackground(UIManager.getColor("List.selectionBackground"));
        panel.setForeground(UIManager.getColor("List.selectionForeground"));
        panel.setBorder(UIManager.getBorder("List.focusCellHighlightBorder"));
        count++;

      } else {
        Border border = null;
        if (panel.isVisible()) {
          if (Platform.isMacOS()) {
            if (count % 2 == 1) {
              border = UIManager.getBorder("List.oddRowBackgroundPainter");
            } else {
              border = UIManager.getBorder("List.evenRowBackgroundPainter");
            }
          } else {
            if (count % 2 == 1) {
              panel.setBackground(new Color(219, 224, 229));
            } else {
              panel.setBackground(new Color(241, 241, 241));
            }
          }
          count++;
        }

        if (border == null) {
          border = BorderFactory.createEmptyBorder(1, 1, 1, 1);
        }
        panel.setBorder(border);
        panel.setForeground(UIManager.getColor("List.foreground"));
      }
    }
  }


  @Override
  public Dimension getPreferredScrollableViewportSize() {
    return getPreferredSize();
  }


  /**
   * Amount to scroll to reveal a new page of items
   */
  @Override
  public int getScrollableBlockIncrement(Rectangle visibleRect,
                                         int orientation, int direction) {
    if (orientation == SwingConstants.VERTICAL) {
      int blockAmount = visibleRect.height;
      if (direction > 0) {
        visibleRect.y += blockAmount;
      } else {
        visibleRect.y -= blockAmount;
      }

      blockAmount +=
        getScrollableUnitIncrement(visibleRect, orientation, direction);
      return blockAmount;
    }
    return 0;
  }


  /**
   * Amount to scroll to reveal the rest of something we are on or a new item
   */
  @Override
  public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
    if (orientation == SwingConstants.VERTICAL) {
      int lastHeight = 0, height = 0;
      int bottomOfScrollArea = visibleRect.y + visibleRect.height;

      for (Component c : getComponents()) {
        if (c.isVisible()) {
          if (c instanceof DetailPanel) {
            Dimension d = c.getPreferredSize();

            int nextHeight = height + d.height;

            if (direction > 0) {
              // scrolling down
              if (nextHeight > bottomOfScrollArea) {
                return nextHeight - bottomOfScrollArea;
              }
            } else {
              // scrolling up
              if (nextHeight > visibleRect.y) {
                if (visibleRect.y != height) {
                  return visibleRect.y - height;
                } else {
                  return visibleRect.y - lastHeight;
                }
              }
            }

            lastHeight = height;
            height = nextHeight;
          }
        }
      }
    }
    return 0;
  }


  @Override
  public boolean getScrollableTracksViewportHeight() {
    return false;
  }


  @Override
  public boolean getScrollableTracksViewportWidth() {
    return true;
  }


  public int getRowCount() {
    return panelByContribution.size();
  }
}
