/*-
 *******************************************************************************
 * Copyright (c) 2011, 2014 Diamond Light Source Ltd.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Matthew Gerring - initial API and implementation and/or initial documentation
 *******************************************************************************/
package org.eclipse.dawnsci.slicing.api;

import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.dawnsci.analysis.api.io.SliceObject;
import org.eclipse.dawnsci.plotting.api.IPlottingSystem;
import org.eclipse.dawnsci.plotting.api.PlotType;
import org.eclipse.dawnsci.slicing.api.system.AxisChoiceEvent;
import org.eclipse.dawnsci.slicing.api.system.AxisChoiceListener;
import org.eclipse.dawnsci.slicing.api.system.AxisType;
import org.eclipse.dawnsci.slicing.api.system.DimensionalEvent;
import org.eclipse.dawnsci.slicing.api.system.DimensionalListener;
import org.eclipse.dawnsci.slicing.api.system.DimsData;
import org.eclipse.dawnsci.slicing.api.system.DimsDataList;
import org.eclipse.dawnsci.slicing.api.system.ISliceGallery;
import org.eclipse.dawnsci.slicing.api.system.ISliceSystem;
import org.eclipse.dawnsci.slicing.api.system.RangeMode;
import org.eclipse.dawnsci.slicing.api.tool.ISlicingTool;
import org.eclipse.dawnsci.slicing.api.util.SliceUtils;
import org.eclipse.january.dataset.IDataset;
import org.eclipse.january.metadata.IMetadata;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IContributionItem;
import org.eclipse.jface.action.IContributionManager;
import org.eclipse.jface.action.IToolBarManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.richbeans.annot.DOEUtils;
import org.eclipse.swt.SWT;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Do not expose this class to copying. Instead use ISliceSystem
 * @author Matthew Gerring
 * @internal
 */
public abstract class AbstractSliceSystem implements ISliceSystem {

	protected static final Logger logger = LoggerFactory.getLogger(AbstractSliceSystem.class);

	protected DimsDataList    dimsDataList;
	protected IPlottingSystem plottingSystem;
	protected String          sliceReceiverId;
	private List<IAction>     customActions;
	protected SliceObject     sliceObject;
	protected RangeMode       rangeMode=RangeMode.NO_RANGES;
	protected Action          advanced;
	
	protected Enum<?>        sliceType=PlotType.IMAGE;
	protected IToolBarManager sliceToolbar;
	
	@Override
	public <T> void setPlottingSystem(IPlottingSystem<T> system) {
		this.plottingSystem = system;
	}

	@Override
	public <T> IPlottingSystem<T> getPlottingSystem() {
		return plottingSystem;
	}
	public SliceObject getCurrentSlice() {
		return sliceObject;
	}

	@Override
	public void setDimsDataList(DimsDataList sliceSetup) {
		this.dimsDataList = sliceSetup;
	}

	@Override
	public DimsDataList getDimsDataList() {
		return dimsDataList;
	}
	
	/**
	 * May be implemented to save the current slice set up.
	 */
	protected abstract void saveSliceSettings();
	
	private ISlicingTool activeTool;

	protected Map<String, ISlicingTool> sliceTools;

	/**
	 * Creates the slice tools by reading extension points
	 * for the slice tools.
	 * 
	 * @return
	 */
	protected IToolBarManager createSliceTools() {
				
		final ToolBarManager man = new ToolBarManager(SWT.FLAT|SWT.RIGHT|SWT.WRAP);
		man.add(new Separator("sliceTools"));
		return createSliceTools(man);
	}

	/**
	 * Creates the slice tools by reading extension points
	 * for the slice tools.
	 * 
	 * @return
	 */
	protected IToolBarManager createSliceTools(final ToolBarManager man) {
				
		final IConfigurationElement[] eles = Platform.getExtensionRegistry().getConfigurationElementsFor("org.eclipse.dawnsci.slicing.api.slicingTool");

  		plotTypeActions= new HashMap<Enum<?>, IAction>();
  		this.sliceTools= new LinkedHashMap<String, ISlicingTool>(17);

		for (IConfigurationElement e : eles) {
			
			final ISlicingTool slicingTool = createSliceTool(e);
			
			final String requireSep = e.getAttribute("separator");
			if ("true".equals(requireSep)) man.add(new Separator());
			
			IAction action = slicingTool.createAction();
			if (action==null) action = createSliceToolAction(e, slicingTool);
			man.add(action);
			plotTypeActions.put(slicingTool.getSliceType(), action);

			sliceTools.put(slicingTool.getToolId(), slicingTool);
		}
								
		return man;
	}
	
	private IAction createSliceToolAction(IConfigurationElement e, final ISlicingTool slicingTool) {
		
		String toolTip = e.getAttribute("tooltip");
		if (toolTip==null) toolTip = slicingTool.getToolId();

		final Action action = new Action(toolTip, IAction.AS_CHECK_BOX) {
        	public void run() {
        		militarize(slicingTool);
        	}
        };
        
    	final String   icon  = e.getAttribute("icon");
    	if (icon!=null) {
	    	final String   id    = e.getContributor().getName();
	    	final Bundle   bundle= Platform.getBundle(id);
	    	final URL      entry = bundle.getEntry(icon);
	    	final ImageDescriptor des = ImageDescriptor.createFromURL(entry);
	    	action.setImageDescriptor(des);
    	}

		action.setId(slicingTool.getToolId());	
	    return action;	
	}
	
	/**
	 * Disarms the current tool (if different) and arms this tool.
	 * 
	 * @param tool
	 */
	@Override
	public void militarize(ISlicingTool slicingTool) {
		saveSliceSettings();
		if (activeTool!=null && slicingTool!=activeTool) {
			activeTool.demilitarize();
		}
		
		// If we don't support advanced slicing (averaging etc.) then disable
		if (advanced!=null) advanced.setEnabled(slicingTool.isAdvancedSupported());
		if (!slicingTool.isAdvancedSupported()) {
            DimsDataList list = getDimsDataList();
            for (DimsData dd : list.iterable()) {
            	if (dd.getPlotAxis().isAdvanced()) {
            		dd.setSliceRange(null);
            		dd.setPlotAxis(AxisType.SLICE);
            	}
			}
		}

		slicingTool.militarize(false);
		activeTool = slicingTool;
		
		// check the correct actions
		for (Enum<?> key : plotTypeActions.keySet()) {
			final IAction action = plotTypeActions.get(key);
			action.setChecked(key==sliceType);
		}
		
	}
	
	/**
	 * 
	 * @return null if ok, error message if errors.
	 */
	protected String checkErrors() {
		
		boolean isX = false;
		for (int i = 0; i < dimsDataList.size(); i++) {
			if (dimsDataList.getDimsData(i).getPlotAxis()==AxisType.X) isX = true;
		}
		boolean isY = false;
		for (int i = 0; i < dimsDataList.size(); i++) {
			if (dimsDataList.getDimsData(i).getPlotAxis()==AxisType.Y) isY = true;
		}
		boolean isYMany = false;
		for (int i = 0; i < dimsDataList.size(); i++) {
			if (dimsDataList.getDimsData(i).getPlotAxis()==AxisType.Y_MANY) isYMany = true;
		}
		boolean isZ = false;
		for (int i = 0; i < dimsDataList.size(); i++) {
			if (dimsDataList.getDimsData(i).getPlotAxis()==AxisType.Z) isZ = true;
		}

		String errorMessage = "";
		boolean ok = false;
		
		int dimCount = getDimensions(getSliceType());

		if (dimCount==1) {
			ok = isX;
			errorMessage = "Please set an X axis.";
		} else if (dimCount==2){
			ok = (isX&&isY&&getSliceType()!=PlotType.XY_STACKED)     ||
				 (isX&&isYMany&&getSliceType()==PlotType.XY_STACKED) ;
			
			errorMessage = !isYMany
					     ? "Please set an X and Y (Many) axis or switch to 'Slice as line plot'."
					     : "Please set an X and Y axis or switch to 'Slice as line plot'.";
		} else if (dimCount==3){
			ok = isX&&isY&&isZ;
			errorMessage = "Please set an X, Y and Z axis or switch to 'Slice as image plot'.";
		}
		
		if (ok) { // Check size of ranges.
			try {
				for (DimsData dd : dimsDataList.iterable()) {
					if (dd.isTextRange()) {
						if (dd.getSliceRange(true)==null) { // Set a range over all the data
							if (rangeMode == RangeMode.MULTI_RANGE) {
								dd.setSliceRange("all");
							} else {
								int max = Math.min(25, getData().getLazySet().getShape()[dd.getDimension()]-1);
								dd.setSliceRange("0:"+max);
							}
							
						}
						final int size = DOEUtils.getSize(dd.getSliceRange(true), null);
						if (size>getData().getLazySet().getShape()[dd.getDimension()] || size<1) {
							errorMessage = "The slice '"+dd.getSliceRange(true)+"' does not fit the data.";
							ok = false;
							break;
						}
					}
				}
			} catch (Exception ignored) {
				// ignore problem 
			}
		}

		return ok ? null : errorMessage;
	}


	protected int getDimensions(Enum<?> st)  {
		
		try {
			final Method dimCountMethod = st.getClass().getMethod("getDimensions");
			final int dimCount = (Integer)dimCountMethod.invoke(st);
			return dimCount;
		} catch (Exception ne) {
			logger.error("Slice type "+st+" must define a method called 'getDimensions'!", ne);
			return 0;
		}
	}

	private  Map<Enum<?>, IAction> plotTypeActions;
	protected IAction getActionByPlotType(Object plotType) {
		if (plotTypeActions==null) return null;
		return plotTypeActions.get(plotType);
	}


	
	/**
	 * 
	 * @param e
	 * @return
	 */
	private ISlicingTool createSliceTool(IConfigurationElement e) {
    	
		ISlicingTool tool = null;
    	try {
    		tool  = (ISlicingTool)e.createExecutableExtension("class");
    	} catch (Throwable ne) {
    		logger.error("Cannot create tool page "+e.getAttribute("class"), ne);
    		return null;
    	}
    	tool.setToolId(e.getAttribute("id"));	       	
    	tool.setSlicingSystem(this);
    	
    	// TODO Provide the tool with a reference to the part with the
    	// slice will end up being showed in?
    	
    	return tool;
	}


	@Override
	public void dispose() {
		if (dimensionalListeners!=null) dimensionalListeners.clear();
		dimensionalListeners = null;
	}

	@Override
	public void setSliceGalleryId(String id) {
		this.sliceReceiverId = id;
	}
	
	protected void openGallery() {
		
		if (sliceReceiverId==null) return;
		SliceObject cs;
		try {
			final SliceObject current = getCurrentSlice();
			cs = SliceUtils.createSliceObject(dimsDataList, getData(), current);
		} catch (Exception e1) {
			logger.error("Cannot create a slice!");
			return;
		}
		
		IViewPart view;
		try {
			view = getActivePage().showView(sliceReceiverId);
		} catch (PartInitException e) {
			logger.error("Cannot find view "+sliceReceiverId);
			return;
		}
		if (view instanceof ISliceGallery) {
			((ISliceGallery)view).updateSlice(getData().getLazySet(), cs);
		}
		
	}
	private static IWorkbenchPage getActivePage() {
		final IWorkbench bench = PlatformUI.getWorkbench();
		if (bench == null)
			return null;
		final IWorkbenchWindow window = bench.getActiveWorkbenchWindow();
		if (window == null)
			return null;
		return window.getActivePage();
	}

	public void addCustomAction(IAction customAction) {
		if (customActions == null)customActions = new ArrayList<IAction>();
		customActions.add(customAction);
	}
	
	protected void createCustomActions(IContributionManager man) {
		if (customActions!=null) {
			man.add(new Separator("group5"));
			for (IAction action : customActions) man.add(action);
		}
	}


	private Collection<DimensionalListener> dimensionalListeners;
	@Override
	public void addDimensionalListener(DimensionalListener l) {
		if (dimensionalListeners==null) dimensionalListeners= new HashSet<DimensionalListener>(7);
		dimensionalListeners.add(l);
	}
	
	@Override
	public void removeDimensionalListener(DimensionalListener l) {
		if (dimensionalListeners==null) return;
		dimensionalListeners.remove(l);
	}
	
	protected void fireDimensionalListeners() {
		if (dimensionalListeners==null) return;
		final DimensionalEvent evt = new DimensionalEvent(this, dimsDataList);
		for (DimensionalListener l : dimensionalListeners) {
			l.dimensionsChanged(evt);
		}
	}
	
	private Collection<AxisChoiceListener> axisChoiceListeners;
	@Override
	public void addAxisChoiceListener(AxisChoiceListener l) {
		if (axisChoiceListeners==null) axisChoiceListeners= new HashSet<AxisChoiceListener>(7);
		axisChoiceListeners.add(l);
	}
	
	@Override
	public void removeAxisChoiceListener(AxisChoiceListener l) {
		if (axisChoiceListeners==null) return;
		axisChoiceListeners.remove(l);
	}
	
	protected void fireAxisChoiceListeners(AxisChoiceEvent evt) {
		if (axisChoiceListeners==null) return;
		for (AxisChoiceListener l : axisChoiceListeners) {
			l.axisChoicePerformed(evt);
		}
	}


	@Override
	public Enum<?> getSliceType() {
		return sliceType;
	}

	@Override
	public void setSliceType(@SuppressWarnings("rawtypes") Enum plotType) {
		this.sliceType = plotType;
		setSliceTypeInfo(null, null);
		checkToolDimenionsOk();
	}
	
	/**
	 * Checks the tools and disables any which require mre dimensions 
	 * than we have
	 */
	protected void checkToolDimenionsOk() {
		
		final int rank = getData().getLazySet().getRank();
        for (Enum<?> type : plotTypeActions.keySet()) {
        	
        	int dims = getDimensions(type);
        	
        	boolean enabled = dims<=rank;
        	if (enabled && sliceActionEnabledMap!=null && sliceActionEnabledMap.containsKey(type)) {
        		if (!sliceActionEnabledMap.get(type)) enabled = false;
        	}
        	plotTypeActions.get(type).setEnabled(enabled);
        }
	}
	

	public void setSliceActionsEnabled(boolean enabled) {
		
		if (sliceToolbar==null) return;
		final IContributionItem[] items = sliceToolbar.getItems();
		for (IContributionItem toolItem : items) {
			if (toolItem instanceof ActionContributionItem) {
				((ActionContributionItem)toolItem).getAction().setEnabled(enabled);
			}
		}
		sliceToolbar.update(true);
		
		if (plotTypeActions!=null) {
			if (sliceActionEnabledMap==null) sliceActionEnabledMap = new HashMap<Enum<?>, Boolean>();
			for (Enum<?> type : plotTypeActions.keySet()) sliceActionEnabledMap.put(type, false);
		}

	}
	
	private Map<Enum<?>, Boolean> sliceActionEnabledMap;
	/**
	 * Throws an NPE if the action is not there.
	 */
	@Override
	public void setSliceActionEnabled(@SuppressWarnings("rawtypes") Enum type, boolean enabled) {
		final IAction action = getActionByPlotType(type);
		action.setEnabled(enabled);
		if (sliceToolbar!=null) sliceToolbar.update(true);
		if (sliceActionEnabledMap==null) sliceActionEnabledMap = new HashMap<Enum<?>, Boolean>();
		sliceActionEnabledMap.put(type, enabled);
	}


	/**
	 * Does nothing by default.
	 */
	@Override
	public void setSliceTypeInfo(String label, ImageDescriptor icon) {
		
	}
	
	/**
	 * 
	 * @return true if the current slice type is a 3D one.
	 */
	public boolean is3D() {
		return sliceType instanceof PlotType && ((PlotType)sliceType).is3D();
	}

	@Override
	public ISlicingTool getActiveTool() {
		return activeTool;
	}
	
	private static final String ADVANCED = "org.dawb.workbench.slicing.component.advanced";

	public boolean isAdvanced() {
		return Activator.getDefault().getPreferenceStore().getBoolean(ADVANCED);
	}
	protected void setAdvanced(boolean advanced) {
		Activator.getDefault().getPreferenceStore().setValue(ADVANCED, advanced);
	}

	public RangeMode getRangeMode() {
		return rangeMode;
	}

	public void setRangeMode(RangeMode rm) {
		this.rangeMode = rm;
	}
	
	private IMetadata sliceMetadata;
	/**
	 * The metadata of the current slice, if any
	 * @return
	 */
	public IMetadata getSliceMetadata() {
		return sliceMetadata;
	}
	
	/**
	 * The metadata of the current slice, if any
	 * @return
	 */
	public void setSliceMetadata(IMetadata sliceMeta) {
		this.sliceMetadata = sliceMeta;
	}

	private IDataset slice;

	public IDataset getSlice() {
		return slice;
	}

	public void setSlice(IDataset slice) {
		this.slice = slice;
	}
	
}