package org.exist.client.xacml;

import com.sun.xacml.Policy;
import com.sun.xacml.PolicySet;
import com.sun.xacml.Rule;
import com.sun.xacml.Target;
import com.sun.xacml.combine.OrderedPermitOverridesPolicyAlg;
import com.sun.xacml.combine.OrderedPermitOverridesRuleAlg;
import com.sun.xacml.combine.PolicyCombiningAlgorithm;
import com.sun.xacml.combine.RuleCombiningAlgorithm;
import com.sun.xacml.ctx.Result;

import java.awt.Container;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.net.URI;
import java.util.Collections;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.xmldb.api.base.Collection;

public class XACMLEditor extends JFrame implements ActionListener, TreeModelListener, TreeSelectionListener, WindowListener
{
	private static final String DEFAULT_DESCRIPTION = "This is a policy template.  It will match and deny everything until you change the target and add rules.";
	private static final String DEFAULT_RULE_DESCRIPTION = "This rule denies everything that is not permitted by the rules above it when " +
	"used with the ordered permit overrides combining algorithm.  Any rules below it will not be evaluated, so it should be the last rule";

	private static final String DEFAULT_POLICY_ID = "NewPolicy";
	private static final String DEFAULT_POLICY_SET_ID = "NewPolicySet";
	private static final String DEFAULT_RULE_ID = "NewRule";
	
	private static final String CLOSE = "Close";
	private static final String SAVE = "Save";
	
	private static final int MIN_FRAME_WIDTH = 600;
	private static final int MIN_FRAME_HEIGHT = 350;
	private static final int MINIMUM_TREE_WIDTH = 100;
		
	private DatabaseInterface dbInterface;
	private XACMLTreeModel model;
	private JTree tree;
	private NodeEditor editor;
	private JSplitPane split;
	
	private XACMLEditor() {}
	public XACMLEditor(Collection systemCollection)
	{
		super("XACML Policy Editor");
		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
		addWindowListener(this);
		
		dbInterface = new DatabaseInterface(systemCollection);
		setupMenuBar(); 
		createInterface();
			
		//setSize(600, 480);
		pack();
		Dimension size = getSize();
		if(size.width <= MIN_FRAME_WIDTH)
			size.width = MIN_FRAME_WIDTH;
		if(size.height <= MIN_FRAME_HEIGHT)
			size.height = MIN_FRAME_HEIGHT;
		setSize(size);
	}
	private void createInterface()
	{
		split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true);
		getContentPane().add(split);
		
		model = new XACMLTreeModel(dbInterface.getPolicies());
		model.addTreeModelListener(this);
		
		
		tree = new JTree(model);
		tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
		TreeMutator mutator = new TreeMutator(tree);
		tree.addTreeSelectionListener(this);
		tree.setCellRenderer(new CustomRenderer(mutator));
		tree.setEditable(false);
		tree.setShowsRootHandles(true);
		tree.setRootVisible(false);
		
		Dimension minSize = tree.getMinimumSize();
		if(minSize.width < MINIMUM_TREE_WIDTH)
			minSize.width = MINIMUM_TREE_WIDTH;
		tree.setMinimumSize(minSize);
				
		JScrollPane scroll = new JScrollPane(tree);
		
		split.setLeftComponent(scroll);
		split.setRightComponent(new JScrollPane());
		split.setOneTouchExpandable(false);
	}
	public void close()
	{
		if(hasUnsavedChanges())
		{
			String message = "There are unsaved changes.  Do you want to save your changes before closing?";
			String title = "Save changes?"; 
			int ret = JOptionPane.showConfirmDialog(this, message, title, JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
			if(ret == JOptionPane.CANCEL_OPTION)
				return;
			else if(ret == JOptionPane.YES_OPTION)
				saveAll();
		}
		dispose();
	}
	public boolean hasUnsavedChanges()
	{
		return model.hasUnsavedChanges();
	}
	public void saveAll()
	{
		if(editor != null)
			editor.pushChanges();
		dbInterface.writePolicies((RootNode)model.getRoot());
		tree.repaint();
	}
	
	public static PolicySet createDefaultPolicySet(PolicyElementContainer parent)
	{
		return createDefaultPolicySet(createUniqueId(parent, DEFAULT_POLICY_SET_ID));
	}
	public static PolicySet createDefaultPolicySet(String policySetID)
	{
		PolicyCombiningAlgorithm alg = new OrderedPermitOverridesPolicyAlg(); 
		return new PolicySet(URI.create(policySetID), alg, createEmptyTarget());
	}

	public static Target createEmptyTarget()
	{
		return new Target(Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_LIST);
	}
	public static Policy createDefaultPolicy(PolicyElementContainer parent)
	{
		return createDefaultPolicy(createUniqueId(parent, DEFAULT_POLICY_ID));
	}
	public static Policy createDefaultPolicy(String policyID)
	{
		Target emptyTarget = createEmptyTarget();
		RuleCombiningAlgorithm alg = new OrderedPermitOverridesRuleAlg(); 
		Rule denyEverythingRule = createDefaultRule("DenyAll");
		List rules = Collections.singletonList(denyEverythingRule);
		return new Policy(URI.create(policyID), alg, DEFAULT_DESCRIPTION, emptyTarget, rules);
	}
	public static Rule createDefaultRule(PolicyElementContainer parent)
	{
		return createDefaultRule(createUniqueId(parent, DEFAULT_RULE_ID));
	}
	public static String createUniqueId(PolicyElementContainer parent, String base)
	{
		if(parent == null)
			throw new NullPointerException("Parent cannot be null");
		String newId = base;
		for(int i = 2; parent.containsId(newId); ++i)
			newId = base + Integer.toString(i);
		return newId;
	}
	public static Rule createDefaultRule(String id)
	{
		return new Rule(URI.create(id), Result.DECISION_DENY, DEFAULT_RULE_DESCRIPTION, null, null);
	}
	
	private void setupMenuBar()
	{
		JMenuBar menuBar = new JMenuBar();
		setJMenuBar(menuBar);
		JMenu file = new JMenu("File");
		file.setMnemonic(KeyEvent.VK_F);
		menuBar.add(file);
		
		JMenuItem saveItem = new JMenuItem(SAVE,KeyEvent.VK_S);
		saveItem.setActionCommand(SAVE);
        saveItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S"));
		saveItem.addActionListener(this);
		file.add(saveItem);

		JMenuItem closeItem = new JMenuItem(CLOSE,KeyEvent.VK_W);
		closeItem.setActionCommand(CLOSE);
		closeItem.setAccelerator(KeyStroke.getKeyStroke("ctrl W"));
		closeItem.addActionListener(this);
		file.add(closeItem);
	}
	public void actionPerformed(ActionEvent event)
	{
		String actionCommand = event.getActionCommand();
		if(CLOSE.equals(actionCommand))
			close();
		else if(SAVE.equals(actionCommand))
			saveAll();
	}
	//this method fixes dumb repaint issues
	//when components are updated
	private void forceRepaint()
	{
		Container contentPane = getContentPane();
		contentPane.invalidate();
		contentPane.validate();
		contentPane.repaint();
	}

	public void valueChanged(TreeSelectionEvent event)
	{
		if(editor != null)
		{
			editor.pushChanges();
			editor = null;
		}
		
		TreePath selectedPath = event.getPath();
		Object value = selectedPath.getLastPathComponent();
		
		if(value instanceof AbstractPolicyNode)
			editor = new AbstractPolicyEditor();
		else if(value instanceof RuleNode)
			editor = new RuleEditor();
		else if(value instanceof ConditionNode)
			editor = null;//TODO: implement condition editing
		else if(value instanceof TargetNode)
			editor = new TargetEditor(dbInterface);
		
		int dividerLocation = split.getDividerLocation();
		JScrollPane scroll = ((JScrollPane)split.getRightComponent());
		if(editor == null)
			scroll.setViewportView(null);
		else				
		{
			editor.setNode((XACMLTreeNode)value);
			scroll.setViewportView(editor.getComponent());
		}
		split.setDividerLocation(dividerLocation);
			
		
		forceRepaint();
	}
	//WindowListener methods
	public void windowActivated(WindowEvent event) {}
	public void windowClosed(WindowEvent event) {}
	public void windowClosing(WindowEvent event)
	{
		close();
	}
	public void windowDeactivated(WindowEvent event) {}
	public void windowDeiconified(WindowEvent event) {}
	public void windowIconified(WindowEvent event) {}
	public void windowOpened(WindowEvent event) {}

	private void treeChanged(TreeModelEvent event)
	{
		tree.revalidate();
		tree.repaint();
	}
	//TreeModelListener methods
	public void treeNodesChanged(TreeModelEvent event)
	{
		treeChanged(event);
	}
	public void treeNodesInserted(TreeModelEvent event)
	{
		treeChanged(event);
	}
	public void treeNodesRemoved(TreeModelEvent event)
	{
		treeChanged(event);
	}
	public void treeStructureChanged(TreeModelEvent event)
	{
		treeChanged(event);
	}
}
