/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2007-2015 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/*
 * Created by JFormDesigner on Thu Oct 31 22:31:02 EDT 2013
 */

package org.broad.igv.util.encode;

import java.awt.*;
import java.awt.event.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.NumberFormatter;

import com.jidesoft.swing.JideBoxLayout;
import org.broad.igv.logging.*;
import org.broad.igv.Globals;
import org.broad.igv.ui.IGV;
import org.broad.igv.util.Pair;
import org.broad.igv.util.ResourceLocator;

/**
 * @author Jim Robinson
 */
public class EncodeFileBrowser extends org.broad.igv.ui.IGVDialog  {

    private static Logger log = LogManager.getLogger(EncodeFileBrowser.class);

    private static Map<String, EncodeFileBrowser> instanceMap = Collections.synchronizedMap(new HashMap<String, EncodeFileBrowser>());
    private static NumberFormatter numberFormatter = new NumberFormatter();

    private JButton cancelButton;
    private JPanel dialogPane;
    private JPanel contentPanel;
    private JScrollPane scrollPane1;
    private JTable table;
    private JPanel filterPanel;
    private JLabel filterLabel;
    private JTextField filterTextField;
    private JLabel rowCountLabel;
    private JPanel buttonBar;
    private JButton okButton;

    EncodeTableModel model;
    private boolean canceled;


    public synchronized static EncodeFileBrowser getInstance(String genomeId) throws IOException {

        String encodeGenomeId = getEncodeGenomeId(genomeId);
        EncodeFileBrowser instance = instanceMap.get(encodeGenomeId);
        if (instance == null) {
            Pair<String [], List<EncodeFileRecord>> records = getEncodeFileRecords(encodeGenomeId);
            if (records == null) {
                return null;
            }
            Frame parent = IGV.hasInstance() ? IGV.getInstance().getMainFrame() : null;
            instance = new EncodeFileBrowser(parent, new EncodeTableModel(records.getFirst(), records.getSecond()));
            instanceMap.put(encodeGenomeId, instance);
        }

        return instance;
    }

    static HashSet<String> supportedGenomes = new HashSet<String>(Arrays.asList("hg19", "mm9"));
    public static boolean genomeSupported(String genomeId) {
          return genomeId != null && supportedGenomes.contains(getEncodeGenomeId(genomeId));
    }

    private static String getEncodeGenomeId(String genomeId) {
        if (genomeId.equals("b37") || genomeId.equals("1kg_v37")) return "hg19";
        else return genomeId;
    }

    private static Pair<String [], List<EncodeFileRecord>> getEncodeFileRecords(String genomeId) throws IOException {

        InputStream is = null;

        try {

            is = EncodeFileBrowser.class.getResourceAsStream("encode." + genomeId + ".txt");
            if (is == null) {
                return null;
            }
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));

            String[] headers = Globals.tabPattern.split(reader.readLine());

            List<EncodeFileRecord> records = new ArrayList<EncodeFileRecord>(20000);
            String nextLine;
            while ((nextLine = reader.readLine()) != null) {
                if (!nextLine.startsWith("#")) {

                    String[] tokens = Globals.tabPattern.split(nextLine, -1);
                    String path = tokens[0];

                    Map<String, String> attributes = new HashMap<String, String>();
                    for (int i = 0; i < headers.length; i++) {
                        String value = i < tokens.length ? tokens[i] : "";
                        if (value.length() > 0) {
                            attributes.put(headers[i], value);
                        }
                    }
                    final EncodeFileRecord record = new EncodeFileRecord(path, attributes);
                    if(record.hasMetaData()) records.add(record);

                }

            }
            return new Pair(headers, records);
        } finally {
            if (is != null) is.close();
        }
    }


    private EncodeFileBrowser(Frame owner, EncodeTableModel model) {
        super(owner);
        this.model = model;
        setModal(true);
        initComponents();
        init(model);
    }

    private void init(final EncodeTableModel model) {
        setModal(true);
        setTitle("Encode Production Data");

        table.setAutoCreateRowSorter(true);
        table.setModel(model);
        table.setRowSorter(model.getSorter());
        try {
            rowCountLabel.setText(numberFormatter.valueToString(table.getRowCount()) + " rows");
        } catch (ParseException e) {

        }

        table.setRowSelectionAllowed(false);
        table.setColumnSelectionAllowed(false);

        filterTextField.getDocument().addDocumentListener(
                new DocumentListener() {
                    public void changedUpdate(DocumentEvent e) {
                        updateFilter();
                    }

                    public void insertUpdate(DocumentEvent e) {
                        updateFilter();
                    }

                    public void removeUpdate(DocumentEvent e) {
                        updateFilter();
                    }
                });

    }

    /**
     * Update the row filter regular expression from the expression in
     * the text box.
     */
    private void updateFilter() {


        RowFilter<EncodeTableModel, Object> rf = null;
        //If current expression doesn't parse, don't update.
        try {
            rf = new RegexFilter(filterTextField.getText());
        } catch (java.util.regex.PatternSyntaxException e) {
            return;
        }
        model.getSorter().setRowFilter(rf);

        try {
            rowCountLabel.setText(numberFormatter.valueToString(table.getRowCount()) + " rows");
        } catch (ParseException e) {
            e.printStackTrace();  //To change body of catch statement use File | Settings | File Templates.
        }
    }


    private void loadButtonActionPerformed(ActionEvent e) {
        canceled = false;
        setVisible(false);
    }

    private void cancelButtonActionPerformed(ActionEvent e) {
        canceled = true;
        setVisible(false);
    }

    public boolean isCanceled() {
        return canceled;
    }

    private Set<String> getLoadedPaths() {

        if (!IGV.hasInstance()) return new HashSet<String>();

        Collection<ResourceLocator> locators = IGV.getInstance().getDataResourceLocators();
        HashSet<String> loadedPaths = new HashSet<String>(locators.size());
        for (ResourceLocator locator : locators) {
            loadedPaths.add(locator.getPath());
        }
        return loadedPaths;
    }

    /**
     * @return the list of VISIBLE selected records.  Filtered records are not returned even if record.selected == true
     * @throws IOException
     */
    public List<EncodeFileRecord> getSelectedRecords() throws IOException {

        List<EncodeFileRecord> selectedRecords = new ArrayList<EncodeFileRecord>();
        List<EncodeFileRecord> allRecords = model.getRecords();

        int rowCount = table.getRowCount();
        for (int i = 0; i < rowCount; i++) {
            int modelIdx = table.convertRowIndexToModel(i);
            EncodeFileRecord record = allRecords.get(modelIdx);
            if (record.isSelected()) {
                selectedRecords.add(record);
                record.setSelected(false);   // Prevent loading twice
            }
        }

        return selectedRecords;
    }

    private class RegexFilter extends RowFilter {

        List<Pair <String, Matcher>> matchers;

        RegexFilter(String text) {

            if (text == null) {
                throw new IllegalArgumentException("Pattern must be non-null");
            }
            matchers = new ArrayList<Pair <String, Matcher>>();
            String[] tokens = Globals.whitespacePattern.split(text);
            for (String t : tokens) {
                // If token contains an = sign apply to specified column only
                String column = "*";
                String value = t.trim();
                if (t.contains("=")) {
                    String[] kv = Globals.equalPattern.split(t);
                    if (kv.length > 1) {
                        column = kv[0].trim();
                        value = kv[1].trim();
                    } else {
                        value = kv[0];  // Value is column name until more input is entered
                    }
                }

                matchers.add(new Pair(column, Pattern.compile("(?i)" + value).matcher("")));
            }

        }

        /**
         * Include row if each matcher succeeds in at least one column.  In other words all the conditions
         * are combined with "and"
         *
         * @param value
         * @return
         */
        @Override
        public boolean include(Entry value) {

            for (Pair<String, Matcher> entry : matchers) {
                String column = entry.getFirst();
                Matcher matcher = entry.getSecond();


                // Search for a match in at least one column.  The first column is the checkbox.
                boolean found = false;  // Pessimistic
                int nColumns = table.getColumnCount();
                for (int index = 1; index < nColumns; index++) {

                    // Include column headings in search.  This is to prevent premature filtering when entering a
                    // specific column condition (e.g. cataType=ChipSeq)
                    matcher.reset(table.getColumnName(index).toLowerCase());
                    if (matcher.find()) {
                        found = true;
                        break;
                    }

                    boolean wildcard = column.equals("*");
                    if (wildcard || column.equalsIgnoreCase(table.getColumnName(index))) {
                        matcher.reset(value.getStringValue(index));
                        if (matcher.find()) {
                            found = true;
                            break;
                        }
                    }
                }
                if (!found) return false;
            }
            return true;  // If we get here we matched them all
        }

    }


    private void initComponents() {

        dialogPane = new JPanel();
        contentPanel = new JPanel();
        scrollPane1 = new JScrollPane();
        table = new JTable();
        filterPanel = new JPanel();
        filterLabel = new JLabel();
        filterTextField = new JTextField();
        rowCountLabel = new JLabel();
        buttonBar = new JPanel();
        okButton = new JButton();
        cancelButton = new JButton();

        getRootPane().setDefaultButton(okButton);

        final String filterToolTip = "Enter multiple filter strings separated by commas.  e.g.  GM12878, ChipSeq";
        filterLabel.setToolTipText(filterToolTip);
        filterTextField.setToolTipText(filterToolTip);

        //======== this ========
        Container contentPane = getContentPane();
        contentPane.setLayout(new BorderLayout());

        //======== dialogPane ========

        dialogPane.setBorder(new EmptyBorder(12, 12, 12, 12));
        dialogPane.setLayout(new BorderLayout());

        //======== contentPanel ========

        contentPanel.setLayout(new BorderLayout(0, 10));

        //======== scrollPane1 ========

        scrollPane1.setViewportView(table);

        contentPanel.add(scrollPane1, BorderLayout.CENTER);

        //======== panel1 ========

        filterPanel.setLayout(new JideBoxLayout(filterPanel, JideBoxLayout.X_AXIS, 5));

        //---- label1 ----
        filterLabel.setText("Filter:");
        filterPanel.add(filterLabel, JideBoxLayout.FIX);

        //---- filterTextField ----
        filterPanel.add(filterTextField, JideBoxLayout.VARY);

        rowCountLabel.setHorizontalAlignment(JLabel.RIGHT);
        JPanel sillyPanel = new JPanel();
        sillyPanel.setLayout(new JideBoxLayout(sillyPanel, JideBoxLayout.X_AXIS, 0));
        sillyPanel.setPreferredSize(new Dimension(100, 28));
        sillyPanel.add(rowCountLabel, JideBoxLayout.VARY);

        filterPanel.add(sillyPanel, JideBoxLayout.FIX);

        contentPanel.add(filterPanel, BorderLayout.NORTH);

        dialogPane.add(contentPanel, BorderLayout.CENTER);

        //======== buttonBar ========

        buttonBar.setBorder(new EmptyBorder(12, 0, 0, 0));
        buttonBar.setLayout(new GridBagLayout());
        ((GridBagLayout) buttonBar.getLayout()).columnWidths = new int[]{0, 85, 80};
        ((GridBagLayout) buttonBar.getLayout()).columnWeights = new double[]{1.0, 0.0, 0.0};

        //---- okButton ----
        okButton.setText("Load");
        okButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                loadButtonActionPerformed(e);
            }
        });
        buttonBar.add(okButton, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                new Insets(0, 0, 0, 5), 0, 0));

        //---- cancelButton ----
        cancelButton.setText("Cancel");
        cancelButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                cancelButtonActionPerformed(e);
            }
        });
        buttonBar.add(cancelButton, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0,
                GridBagConstraints.CENTER, GridBagConstraints.BOTH,
                new Insets(0, 0, 0, 0), 0, 0));

        dialogPane.add(buttonBar, BorderLayout.SOUTH);

        contentPane.add(dialogPane, BorderLayout.CENTER);
        setSize(700, 620);
        setLocationRelativeTo(getOwner());
    }


    public static void main(String[] args) throws IOException {
        getInstance("hg19").setVisible(true);
    }

}
