/*
 * DataModel.java
 *
 * Created on February 17, 2006, 2:30 PM
 *
 */
package usda.weru.wmrm;

import com.klg.jclass.table.*;
import com.klg.jclass.table.data.*;
import com.klg.jclass.util.JCListenerList;
import de.schlichtherle.truezip.file.TFile;
import java.io.Serializable;
import java.util.*;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.tree.*;
import javax.swing.event.*;
import usda.weru.wmrm.TableMeta.TableNamedDataModel;

/**
 *
 * @author Joseph Levin
 */
public final class DataModel extends AbstractDataSource implements TableNamedDataModel, TreeModel {

    private static final long serialVersionUID = 1L;

    private final RunGroup c_rootGroup;
    private final RunGroup c_currentProject;
    private final RunGroup c_otherRuns;
    private final RunGroup c_projects;
    private RunGroup c_selectedGroup;
    private RunGroup dirGroup;
    private List<RunWrapper> c_selectedRuns;
    //private final RunsWatcher c_watcher;
    private List<TreeModelListener> c_treeModelListeners;
    private List<String> c_dataKeys;

    private Map<TFile, RunGroup> c_groupLookup;

    private void init() {
        c_groupLookup = new HashMap<>();
        c_treeModelListeners = new Vector<>();
    }

    /**
     *
     */
    public DataModel() {
        init();
        //Create root node for the tree;
        c_rootGroup = new RunGroup("All Run Summaries");
        c_currentProject = new RunGroup("Current Project (none)");
        c_currentProject.setSortPriority(0);
        c_currentProject.setParent(c_rootGroup);
        c_currentProject.setExcludeFromRecursion(true);
        c_rootGroup.addChild(c_currentProject);

        c_projects = new RunGroup("Directories");
        c_projects.setSortPriority(2);
        c_projects.setParent(c_rootGroup);
        c_rootGroup.addChild(c_projects);

        c_otherRuns = new RunGroup("Single Runs");
        c_otherRuns.setSortPriority(3);
        c_otherRuns.setParent(c_rootGroup);
        c_rootGroup.addChild(c_otherRuns);

        selectRunGroup(c_rootGroup);
        //c_watcher = new RunsWatcher(this);
        //c_watcher.start();

    }
    
    public RunGroup getRootGroup() {
        return c_rootGroup;
    }

    /**
     * Select a node, table data will reflect the selected tree node.
     * @param group
     */
    public void selectRunGroup(RunGroup group) {
        selectRunGroups(group);
    }

    public void selectRunGroups(RunGroup... groups) {
        switch (groups.length) {
            case 0:
                c_selectedGroup = null;
                return;
            case 1:
                c_selectedGroup = groups[0];
                break;
            default:
                c_selectedGroup = new RunGroup("Selected");
                for (RunGroup group : groups) {
                    c_selectedGroup.addChild(group);
                }   break;
        }
        updateSelectedRuns();
    }

    private void updateSelectedRuns() {
        c_selectedRuns = c_selectedGroup.getRuns(true);
        //Tell the table the data has updated
        fireDataReset();
    }

    @Override
    public void fireDataReset() {
        if (SwingUtilities.isEventDispatchThread()) {
            super.fireDataReset();
        } else {
            SwingUtilities.invokeLater(new Runnable() {

                @Override
                public void run() {
                    DataModel.super.fireDataReset();
                }

            });
        }
    }

    public void setCurrentProject(RunGroup runGroup) {
        if (runGroup == null) {
            //System.err.println("Run Group is NULL!");
            return;
        }
        c_currentProject.setName("[ " + runGroup.toString() + " ]");
        c_currentProject.setChildren(runGroup.getChildren());
        c_currentProject.setRuns(runGroup.getRuns(false));
        fireTreeNodesChanged(c_currentProject);
    }

    public void addRunFileToModel(RunGroup group, TFile file) {
        if (file == null) {
            return;
        }

        RunWrapper run = new RunWrapper(file.getPath(), true);
        if (group == null) {
            //Try to find the directory group
            group = lookupRunGroup(new TFile(file.getParentFile()));
            if (group == null) {
                //No luck, add it as a single run.
                group = c_otherRuns;
            }
        }
        RunGroup wrapperGroup = new RunGroup(file.getName());
        wrapperGroup.addRun(run);
        group.addChild(wrapperGroup);
        wrapperGroup.setParent(group);
        fireTreeNodesInserted(wrapperGroup);

        updateSelectedRuns();
    }

    public RunGroup lookupRunGroup(TFile directory) {
        return c_groupLookup.get(directory);
    }

    public RunGroup addDirectoryToModel(TFile directory) {
        // Commented out any thread related code which was causing issues
        dirGroup = null;
        
        if (directory == null || !directory.exists()) {
            JOptionPane.showMessageDialog(null, "Location not found:\n" + directory.getPath(),
                    "Path Warning", JOptionPane.WARNING_MESSAGE);
            return dirGroup;
        }
        if (!directory.isDirectory()) {
            return dirGroup;
        }
        
        if (lookupRunGroup(directory) != null) {
            dirGroup = lookupRunGroup(directory);
            return dirGroup;
        }

        RunFileFilter filter = new RunFileFilter(true);
        if (filter.accept(directory, false)) {
            addRunFileToModel(null, directory);
            return dirGroup;
        }
         
        dirGroup = new RunGroup(directory.getName());
        dirGroup.setSourceFile(directory);

        RunGroup parent = c_groupLookup.get(directory.getParentFile());
        if (parent == null) {
            parent = c_projects;
        }

        //For adding other weps runs with the same dir name but different path - EL
        if(parent.getChildren().contains(dirGroup)) {
            if(!parent.parentContainsDirectory(directory.getPath())) {
                dirGroup = new RunGroup(directory.getPath());
                dirGroup.setSourceFile(directory);   
            }
        }

        dirGroup.setParent(parent);
        parent.addChild(dirGroup);

        c_groupLookup.put(directory, dirGroup);

        fireTreeNodesInserted(dirGroup);

        TFile[] children;
        children = directory.listFiles(filter);
        if(children != null) {
            for (TFile child : children) {
                if (filter.accept(child, false)) {
                    addRunFileToModel(dirGroup, child);
                } else if (child.isDirectory()) {
                    addDirectoryToModel(child);
                }
            }
        }

        //Sort
        Collections.sort(c_projects.getChildren());
        fireTreeStructureChanged(c_projects);
        return dirGroup;
    }


    public void removeRunGroupFromModel(RunGroup group) {
        RunGroup parent = group.getParent();
        group.setParent(null);
        parent.removeChild(group);
        if (c_selectedGroup == group) {
            selectRunGroup(parent);
        }
        fireTreeStructureChanged(parent);
        updateSelectedRuns();
    }

    private synchronized void buildDataKeyList() {
        List<String> keys = new LinkedList<>();
        c_selectedRuns.forEach(run -> {
            for (String key : run.getDataKeys()) {
                if (!keys.contains(key)) {
                    keys.add(key);
                }
            }
        });
        c_dataKeys = keys;
    }

    private String getDataKeyFromIndex(int columnIndex) {
        switch (columnIndex) {
            case 0:
                return RunWrapper.DataTag.RunName.getFirstTag();
            case 1:
                return RunWrapper.DataTag.RunLocation.getFirstTag();
            case 2:
                return RunWrapper.DataTag.ClientName.getFirstTag();
            case 3:
                return RunWrapper.DataTag.FarmNo.getFirstTag();
            case 4:
                return RunWrapper.DataTag.TractNo.getFirstTag();
            case 5:
                return RunWrapper.DataTag.FieldNo.getFirstTag();
            case 6:
                return RunWrapper.DataTag.ManagementName.getFirstTag();
            case 7:
                return RunWrapper.DataTag.SoilName.getFirstTag();
            case 8:
                return RunWrapper.DataTag.FieldSize.getFirstTag();
            default:
                try {
                    if (c_dataKeys == null || c_dataKeys.isEmpty()) {
                        buildDataKeyList();
                    }
                    return c_dataKeys.get(columnIndex);
                } catch (NullPointerException npe) {
                    return null;
                } catch (ArrayIndexOutOfBoundsException aioobe) {
                    return "";
                } catch (IndexOutOfBoundsException ioobe) {
                    return "";
                }
        }

    }

    //JCTable Interface ************************************************
    @Override
    public Object getTableDataItem(int rowIndex, int columnIndex) {
        String dataKey = getDataKeyFromIndex(columnIndex);
        return getTableDataItem(rowIndex, dataKey);
    }
    
    @Override
    public boolean setTableDataItem(Object obj, int rowIndex, int columnIndex) {
        return true;
    }

    @Override
    public Object getTableDataItem(int rowIndex, String column) {
        try {
            Object value = c_selectedRuns.get(rowIndex).getValue(column);
            return value;
        } catch (ArrayIndexOutOfBoundsException aioob) {
            return null;
        }
    }

    public String getTableDataToolTipText(int rowIndex, int columnIndex) {
        String dataKey = getDataKeyFromIndex(columnIndex);
        try {
            return c_selectedRuns.get(rowIndex).getToolTipText(dataKey);
        } catch (ArrayIndexOutOfBoundsException aioob) {
            return null;
        }
    }

    public RunWrapper getRunWrapper(int rowIndex) {
        if(c_selectedRuns == null) {
            System.out.println("ERROR: There are no selected runs.");
            return null;
        }
        if(c_selectedRuns.size() <= rowIndex) {
            System.out.println("ERROR: provided index is out of bounds");
            return null;
        }
        return c_selectedRuns.get(rowIndex);
    }
    
    public void addRunWrapper(RunWrapper run) {
        System.out.println("adding run");
        c_selectedRuns.add(run);
    }
    
    public boolean removeRunWrapper(int rowIndex) {
        if (c_selectedRuns.get(rowIndex).exists()) {
            c_selectedRuns.remove(rowIndex);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public int getNumRows() {
        return c_selectedRuns.size();
    }

    @Override
    public int getNumColumns() {
        if (c_dataKeys == null) {
            buildDataKeyList();
        }
        if (c_dataKeys != null) {
            return c_dataKeys.size();
        } else {
            return 0;
        }

    }

    @Override
    public Object getTableRowLabel(int rowIndex) {
        return rowIndex;
    }

    @Override
    public Object getTableColumnLabel(int columnIndex) {
        if (c_dataKeys != null) {
            return c_dataKeys.get(columnIndex);
        } else {
            return Integer.toString(columnIndex);
        }
    }

    //********************************************
    //JTree Interface ************************************************
    /**
     *
     * @param parent
     * @param index
     * @return
     */
    @Override
    public Object getChild(Object parent, int index) {
        try {
            RunGroup parentGroup = (RunGroup) parent;
            return parentGroup.getChild(index);
        } catch (ArrayIndexOutOfBoundsException aioob) {
            return null;
        }
    }

    @Override
    public int getChildCount(Object parent) {
        RunGroup parentGroup = (RunGroup) parent;
        return parentGroup.getChildCount();
    }

    @Override
    public int getIndexOfChild(Object parent, Object child) {
        try {
            RunGroup parentGroup = (RunGroup) parent;
            RunGroup childGroup = (RunGroup) child;
            return parentGroup.getIndexOfChild(childGroup);
        } catch (ArrayIndexOutOfBoundsException aioob) {
            return 0;
        }
    }

    @Override
    public Object getRoot() {
        return c_rootGroup;
    }

    @Override
    public boolean isLeaf(Object node) {
        RunGroup group = (RunGroup) node;
        return group.getChildCount() == 0;
    }

    @Override
    public void addTreeModelListener(TreeModelListener l) {
        c_treeModelListeners.add(l);
    }

    @Override
    public void removeTreeModelListener(TreeModelListener l) {
        c_treeModelListeners.remove(l);
    }

    @Override
    public void valueForPathChanged(TreePath path, Object newValue) {

    }

    //Tree events
    public void fireTreeStructureChanged(RunGroup group) {
        TreeModelEvent event = new TreeModelEvent(this, getPathToNode(group));
        for (TreeModelListener listener : c_treeModelListeners) {
            listener.treeStructureChanged(event);
        }
    }

    public void fireTreeNodesChanged(RunGroup group) {
        TreeModelEvent event = new TreeModelEvent(this, getPathToNode(group));
        for (TreeModelListener listener : c_treeModelListeners) {
            listener.treeNodesChanged(event);
        }
    }

    public void fireTreeNodesInserted(RunGroup group) {
        TreePath parentPath = getPathToNode(group.getParent());
        int[] childIndices = {getIndexOfChild(group.getParent(), group)};
        Object[] children = {group};
        TreeModelEvent event = new TreeModelEvent(this, parentPath, childIndices, children);
        for (TreeModelListener listener : c_treeModelListeners) {
            listener.treeNodesInserted(event);
        }
    }

    public void fireTreeNodesRemoved(RunGroup group) {
        TreeModelEvent event = new TreeModelEvent(this, getPathToNode(group.getParent()));
        for (TreeModelListener listener : c_treeModelListeners) {
            listener.treeNodesRemoved(event);
        }
    }

    //********************************************

    public TreePath getPathToNode(RunGroup group) {
        Vector<RunGroup> path = new Vector<RunGroup>();
        while (group != null) {
            path.add(group);
            group = group.getParent();
        }
        Collections.reverse(path);
        try {
            return new TreePath(path.toArray());
        } catch(IllegalArgumentException e) {
            System.out.println("Error: Path is empty or null.");
            return null;
        }
    }

    @Override
    public void addTableDataListener(JCTableDataListener jCTableDataListener) {
        listeners = JCListenerList.add(listeners, jCTableDataListener);
    }

    static class RunGroup implements Comparable<RunGroup>, Serializable {

        private static final long serialVersionUID = 1L;

        private List<RunGroup> c_children;
        private List<RunWrapper> c_runs;
        private boolean c_excludeFromRecursion = false;
        private boolean c_removeFlag;
        private int c_sortPriority = 100;        //Low = at the top of the tree;
        private TFile c_sourceFile;

        public RunGroup(String name) {
            c_runs = Collections.synchronizedList(new LinkedList<>());
            c_name = name;
        }

        public Vector<RunWrapper> getRuns() {
            return getRuns(false);
        }

        public Vector<RunWrapper> getRuns(boolean recursive) {
            Vector<RunWrapper> tempRunList = new Vector<>();
            for (RunWrapper run : c_runs) {
                if (!tempRunList.contains(run)) {
                    tempRunList.add(run);
                }
            }
            if (recursive) {
                List<RunGroup> children = getChildren();
                synchronized (children) {
                    for (RunGroup group : children) {
                        if (group.getExcludeFromRecursion()) {
                            continue;
                        }
                        for (RunWrapper run : group.getRuns(true)) {
                            if (!tempRunList.contains(run)) {
                                tempRunList.add(run);
                            }
                        }
                    }
                }
            }
            return tempRunList;
        }
        

        public void addChild(RunGroup child) {
            //We don't want to add the group if we already have it in our model                
            if (!getChildren().contains(child)) {
                //synchronize access to the children
                getChildren().add(child);
            }
        }

        public List<RunGroup> getChildren() {
            synchronized (this) {
                if (c_children == null) {
                    c_children = Collections.synchronizedList(new LinkedList<>());
                }
                return c_children;
            }
        }

        public void setChildren(List<RunGroup> children) {
            synchronized (this) {
                c_children = children;
            }
        }

        public int getIndexOfChild(RunGroup child) {
            return getChildren().indexOf(child);
        }

        public int getChildCount() {
            return getChildren().size();
        }

        public RunGroup getChild(int index) {
            synchronized (getChildren()) {
                return getChildren().get(index);
            }
        }

        public void removeChild(final RunGroup child) {
            getChildren().remove(child);

        }
        
        public boolean parentContainsDirectory(String path) {
            return this.getChildren().stream().anyMatch(rg -> (path.equals(rg.getSourceFile().getPath())));
        }

        public boolean getExcludeFromRecursion() {
            return c_excludeFromRecursion;
        }

        public void setExcludeFromRecursion(boolean exclude) {
            c_excludeFromRecursion = exclude;
        }

//        public Vector <RunGroup> getChildren(){
//            return c_children;
//        }     
        public void addRun(RunWrapper run) {
            c_runs.add(run);
        }

        private String c_name = "Unknown";

        @Override
        public int hashCode() {
            return c_name.hashCode();
        }
        private RunGroup c_parent = null;

        public void setSourceFile(TFile file) {
            c_sourceFile = file;
        }

        public TFile getSourceFile() {
            return c_sourceFile;
        }

        public boolean getRemoval() {
            return c_removeFlag;
        }

        public void markRemoval(boolean remove) {
            c_removeFlag = remove;
        }

        public boolean containsRunFile(TFile runFile) {
            runFile = new TFile(runFile.getAbsoluteFile());
            return containsRunFile(this, runFile);
        }

        private boolean containsRunFile(RunGroup group, TFile runFile) {
            synchronized (this) {
                for (RunWrapper run : group.getRuns()) {
                    if (run.getDirectory().getAbsoluteFile().equals(runFile)) {
                        return true;
                    }
                }
                List<RunGroup> children = group.getChildren();
                synchronized (children) {
                    Iterator<RunGroup> i = children.iterator();
                    while (i.hasNext()) {
                        RunGroup child = i.next();
                        if (containsRunFile(child, runFile)) {
                            return true;
                        }
                    }
                }
                return false;
            }
        }

        public RunGroup getParent() {
            return c_parent;
        }

        public void setParent(RunGroup parent) {
            c_parent = parent;
        }

        @Override
        public String toString() {
            return c_name;
        }

        public void setName(String name) {
            c_name = name;
        }

        public void setRuns(Vector<RunWrapper> runs) {
            c_runs = runs;
        }

        public int getSortPriority() {
            return c_sortPriority;
        }

        public void setSortPriority(int priority) {
            c_sortPriority = priority;
        }

        @Override
        public int compareTo(RunGroup group) {
            //RunGroup group = (RunGroup) o;
            int p1 = getSortPriority();
            int p2 = group.getSortPriority();
            if (p1 == p2) {
                //Same priority, now by string
                return toString().compareTo(group.toString());
            } else {
                //Not the same priority;
                if (p1 > p2) {
                    return 1;
                } else {
                    return -1;
                }
            }
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof RunGroup)) {
                return false;
            }
            return (this.compareTo((RunGroup) obj) == 0);
        }
    }
}
