package com.excelsior.xds.ui.preferences.sdk;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.lang.StringUtils;
import org.eclipse.jface.dialogs.IInputValidator;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.viewers.CellLabelProvider;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeColumn;
import org.eclipse.swt.widgets.TreeItem;

import com.excelsior.xds.core.sdk.Sdk;
import com.excelsior.xds.core.sdk.SdkTool;
import com.excelsior.xds.ui.commons.utils.SWTFactory;
import com.excelsior.xds.ui.commons.utils.SwtUtils;
import com.excelsior.xds.ui.images.ImageUtils;
import com.excelsior.xds.ui.internal.nls.Messages;

public class SdkToolsControl extends Composite {
    private TreeViewer treeViewer;
    private TreeColumn columnName;
    private TreeColumn columnPath;
    private Button btnAdd;
    private Button btnEdit;
    private Button btnRemove;
    private Button btnUp;
    private Button btnDn;
    private Sdk editedSdk;

    private ArrayList<ModelItem> treeModel;

    /**
     * Create the composite.
     * @param parent
     * @param style
     * @param editedSdk 
     */
    public SdkToolsControl(Composite parent, int style, final Sdk editedSdk) {
        super(parent, style);
        this.editedSdk = editedSdk;
        sdkToTreeModel(editedSdk);

        setLayout(SwtUtils.removeMargins(new GridLayout(2, false)));
        setLayout(SwtUtils.removeMargins(new GridLayout(2, false)));

        // Tree
        treeViewer= new TreeViewer(this, SWT.SINGLE | SWT.BORDER | SWT.FULL_SELECTION);
        treeViewer.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1));
        treeViewer.setLabelProvider(new ToolCellLabelProvider());
        treeViewer.setContentProvider(new ToolListContentProvider());
        treeViewer.setInput(editedSdk);
        treeViewer.getTree().addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent e) {
                handleTreeSelection();
            }        
        });
        treeViewer.setAutoExpandLevel(2);
        Tree tree = treeViewer.getTree();
        columnName = new TreeColumn(tree, SWT.LEFT);
        columnName.setWidth(SwtUtils.getTextWidth(tree, "WWWWWWWWWWWW")); //$NON-NLS-1$
        columnName.setText(Messages.SdkToolsControl_ToolName);
        columnPath = new TreeColumn(tree, SWT.LEFT);
        columnPath.setWidth(SwtUtils.getTextWidth(tree, "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW")); //$NON-NLS-1$
        columnPath.setText(Messages.SdkToolsControl_Location);
        tree.setHeaderVisible(true);
        //tree.setLinesVisible(true);

        // Buttons column
        Composite buttonsComposite = new Composite(this, SWT.NONE);
        buttonsComposite.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 1, 1));
        buttonsComposite.setLayout(SwtUtils.removeMargins(new GridLayout(1, false)));

        btnAdd = SWTFactory.createPushButton(buttonsComposite, Messages.Common_Add, null);
        btnAdd.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                handleAdd();
            }
        });

        btnEdit = SWTFactory.createPushButton(buttonsComposite, Messages.Common_Edit, null);
        btnEdit.setEnabled(false);
        btnEdit.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                handleEdit();
            }
        });

        btnRemove = SWTFactory.createPushButton(buttonsComposite, Messages.Common_Remove, null);
        btnRemove.setEnabled(false);
        btnRemove.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                handleRemove();
            }
        });

        SWTFactory.createVerticalSpacer(buttonsComposite, 0.5);

        btnUp = SWTFactory.createPushButton(buttonsComposite, Messages.SdkToolsControl_Up, null);
        btnUp.setEnabled(false);
        btnUp.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                handleMove(true);
            }
        });

        btnDn = SWTFactory.createPushButton(buttonsComposite, Messages.SdkToolsControl_Down, null);
        btnDn.setEnabled(false);
        btnDn.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                handleMove(false);
            }
        });

        treeViewer.setInput(editedSdk);
        selectModelItem(0);
    }

    private ArrayList<ArrayList<ModelItem>> makeSplitModel(int curIdx, int chunk_offs[]) {
        ArrayList<ArrayList<ModelItem>> splitModel = new ArrayList<ArrayList<ModelItem>>();
        // Split model by top-level items
        for (int i=0; i<treeModel.size(); ++i) {
            ModelItem it = treeModel.get(i);
            if (it.isRoot() || StringUtils.isEmpty(it.tool.getMenuGroup())) {
                splitModel.add(new ArrayList<ModelItem>());
            }
            splitModel.get(splitModel.size()-1).add(it);
            if (i == curIdx && chunk_offs!=null) {
                chunk_offs[0] = splitModel.size()-1;
                chunk_offs[1]  = splitModel.get(chunk_offs[0]).size()-1;
            }
        }
        return splitModel;
    }

    private void handleMove(boolean up) {
        int curIdx = getSelectedIdx();
        if (curIdx < 0) {
            return; // int err?
        }
        ModelItem selectedItem = treeModel.get(curIdx);

        int chunk_offs[] = {0,0}; 
        ArrayList<ArrayList<ModelItem>> splitModel = makeSplitModel(curIdx, chunk_offs);
        int chunk = chunk_offs[0];
        int offs  = chunk_offs[1];

        int step = 0;
        if (offs == 0) {
            // Move whone top-level item:
            ArrayList<ModelItem> cut = null;
            if (chunk > 0 && up) {
                cut = splitModel.get(chunk);
                step = -1;
            } else if (chunk < splitModel.size() - 1 && !up) {
                cut = splitModel.get(chunk);
                step = 1;
            }

            if (cut != null) {
                splitModel.remove(chunk);
                chunk += step;
                splitModel.add(chunk, cut);
            }
        } else {
            // It is item inside chunk - move it here:
            ArrayList<ModelItem> al = splitModel.get(chunk);
            ModelItem cut = null;
            if (offs > 0 && up) {
                cut = al.get(offs);
                step = -1;
            } else if (offs < al.size() - 1 && !up) {
                cut = al.get(offs);
                step = 1;
            }

            if (cut != null) {
                al.remove(offs);
                offs += step;
                al.add(offs, cut);
            }
        }

        if (step != 0) {
            // It was real movement. Put results back to model and show it: 
            treeModel.clear();
            int sel = 0;
            for (ArrayList<ModelItem> al : splitModel) {
                for (ModelItem it : al) {
                    if (it.equals(selectedItem)) {
                        sel = treeModel.size();
                    }
                    treeModel.add(it);
                }
            }
            updateTree(sel);
        }
    }

    private void handleTreeSelection() {
        int curIdx = getSelectedIdx();
        boolean isSep = false;
        boolean isRoot = false;
        int[] curRZ = null;
        if (curIdx >= 0) {
            ModelItem it = treeModel.get(curIdx);
            isRoot = it.isRoot();
            isSep = !isRoot && it.tool.isSeparator();
            curRZ = getRootZone(treeModel.get(curIdx));
        }

        boolean enRem = false;
        boolean enUp  = (curRZ == null ? curIdx > 0 : curIdx != curRZ[0]+1 );
        boolean enDn  = false;
        boolean enEd  = false;
        if (curIdx >= 0) {
            enEd  = !isSep;
            enRem = !isRoot || (curRZ != null && curRZ[0]==curRZ[1]); // no root or empty root 
            if (isRoot) {
                enDn = curRZ[1] < treeModel.size() - 1;
            } else if (curRZ == null) {
                enDn = curIdx < treeModel.size() - 1;
            } else {
                enDn = curIdx < curRZ[1];
            }
        }
        btnRemove.setEnabled(enRem);
        btnUp.setEnabled(enUp);
        btnDn.setEnabled(enDn);
        btnEdit.setEnabled(enEd);
    }

    private int getSelectedIdx() {
        TreeItem[] sel = treeViewer.getTree().getSelection();
        if (sel.length > 0) {
            return treeModel.indexOf(sel[0].getData());
        }
        return -1;
    }

    private TreeItem searchTreeItemForModelItem(TreeItem[] arr, ModelItem it) {
        if (arr != null) {
            for (TreeItem ti : arr) {
                if (ti.getData().equals(it)) {
                    return ti;
                }
                TreeItem tichild = searchTreeItemForModelItem(ti.getItems(), it);
                if (tichild != null) {
                    return tichild;
                }
            }
        }
        return null;
    }

    private void selectModelItem(int idx) {
        try {
            treeViewer.getTree().deselectAll();
            TreeItem ti = searchTreeItemForModelItem(treeViewer.getTree().getItems(), treeModel.get(idx));
            if (ti != null) {
                treeViewer.getTree().select(ti);
                handleTreeSelection();
            }
        } catch (Exception e) {
        }
    }



    private void handleEdit(){
        int idx = getSelectedIdx();
        if (idx >= 0) {
            ModelItem it = treeModel.get(idx);
            if (it.isRoot()) {
                String oldGroup = it.rootName;
                HashSet<String> usedNames = new HashSet<String>();
                for (ModelItem x : treeModel) {
                    if (x.isRoot() && x != it) {
                        usedNames.add(x.rootName);
                    }
                }
                String s = editGroupName(it.rootName, usedNames);
                if (s != null && !s.equals(oldGroup)) {
                    it.rootName = s;
                    for (ModelItem x : treeModel) {
                        if (!x.isRoot() && oldGroup.equals(x.tool.getMenuGroup())) {
                            x.tool.setMenuGroup(s);
                        }
                    }
                    updateTree(idx);
                }
            } else {
                SdkTool tool = it.tool;
                String oldGroup = tool.getMenuGroup();
                SdkTool clonedTool = tool.clone();
                EditSdkToolDialog editSdkToolDialog = new EditSdkToolDialog(clonedTool, editedSdk, collectGroups());
                WizardDialog dialog = new WizardDialog(getShell(), editSdkToolDialog);
                if (dialog.open() == WizardDialog.OK) {
                    tool.copyFrom(clonedTool);
                    String newGroup = tool.getMenuGroup();

                    if (!(oldGroup == null ? "" : oldGroup).equals(newGroup)) { //$NON-NLS-1$

                        int newPos = 0;
                        for (int i=0; i<treeModel.size(); ++i) {
                            if (i != idx) {
                                ModelItem x = treeModel.get(i);
                                if (newGroup.equals(x.isRoot() ? x.rootName : x.tool.getMenuGroup())) {
                                    newPos = i+1; // position after last item of the selected group
                                }
                            }
                        }

                        if (newPos < idx) {
                            treeModel.remove(idx);
                            treeModel.add(newPos, it);
                            idx = newPos;
                        } else if (idx < newPos) {
                            treeModel.add(newPos, it);
                            treeModel.remove(idx);
                            idx = newPos-1;
                        }
                    }
                    updateTree(idx);
                }
            }
        }
    }

    private void handleAdd() {
        int curIdx = getSelectedIdx();
        int[] curRZ = null;
        String curGroup = ""; //$NON-NLS-1$
        if (curIdx >= 0) {
            curRZ = getRootZone(treeModel.get(curIdx));
            if (curRZ != null) {
                curGroup = treeModel.get(curRZ[0]).rootName;
            }
        }
        int newIdx = -1;

        SdkTool newTool = new SdkTool(editedSdk);
        newTool.setMenuGroup(curGroup);
        SdkToolsControlAddDialog addDlg = new SdkToolsControlAddDialog(newTool, editedSdk, collectGroups());
        WizardDialog  addWDlg = new WizardDialog(getShell(), addDlg);
        if (addWDlg.open() == WizardDialog.OK) {
            switch (addDlg.getResult()) {
            case TOOL:
                newIdx = curIdx + 1;
                treeModel.add(newIdx, new ModelItem(newTool));
                break;
            case SEPARATOR: {
                SdkTool sep = new SdkTool(); // new SdkTool() makes separator, not a tool
                sep.setMenuGroup(curGroup);
                newIdx = curIdx + 1;
                treeModel.add(newIdx, new ModelItem(sep)); 
                break;
            }
            case GROUP:
            {
                String name = addDlg.getGroupName();
                if (!StringUtils.isEmpty(name)) {
                    newIdx = curRZ == null ? curIdx + 1 : curRZ[1] + 1;
                    treeModel.add(newIdx, new ModelItem(name));
                    treeViewer.refresh();
                }
            }
			default:
				break;
            } // SWITCH

            updateTree(newIdx);
        }
    }


    private void handleRemove() {
        int curIdx = getSelectedIdx();
        int newIdx = -1;
        if (curIdx >= 0) {
            int[] curRZ = getRootZone(treeModel.get(curIdx));
            if (curRZ == null || curIdx > curRZ[0] || curRZ[0] == curRZ[1]) { 
                treeModel.remove(curIdx);
                newIdx = curIdx-1;
                if (curRZ != null && curRZ[0]+1==curRZ[1]) {
                    treeModel.remove(curRZ[0]); // group becomes empty - remove it
                    --newIdx;
                }
                if (newIdx < 0 && !treeModel.isEmpty()) {
                    newIdx = 0;
                }
            }
        }
        updateTree(newIdx);
    }


    private String editGroupName(String initialName, final Set<String> usedNames) {
        InputDialog dlg = new InputDialog(getShell(), Messages.SdkToolsControl_NewGroupName, 
                Messages.SdkToolsControl_EnterGroupName+':', initialName, 
                new IInputValidator() 
        {
            @Override
            public String isValid(String newText) {
                newText = newText.trim();
                if (newText.isEmpty()) {
                    return Messages.SdkToolsControl_NameIsEmpty;
                } else if (usedNames.contains(newText)) {
                    return Messages.SdkToolsControl_NameIsUsed;
                }
                return null;
            }
        });
        if (dlg.open() == Window.OK) {
            return dlg.getValue().trim();
        }
        return null;
    }

    //      public static void suppressBadSeparators(Sdk sdk) {
    //          while (true) {
    //              List<SdkTool> lst = sdk.getTools();
    //              int sz = lst.size(); 
    //              if (sz == 0) {
    //                  return;
    //              }
    //              if (lst.get(0).isSeparator()) {
    //                  sdk.removeTool(lst.get(0));
    //                  continue;
    //              }
    //            if (lst.get(sz-1).isSeparator()) {
    //                sdk.removeTool(lst.get(sz-1));
    //                continue;
    //            }
    //            for (int i=0; i<sz-1; ++i) {
    //                if (lst.get(i).isSeparator() && lst.get(i+1).isSeparator()) {
    //                    sdk.removeTool(lst.get(i));
    //                    continue;
    //                }
    //            }
    //            return;
    //          }
    //      }
    //      

    private List<String> collectGroups() {
        List<String> lst = new ArrayList<String>();
        Set<String> dups = new HashSet<String>();
        for (ModelItem it : treeModel) {
            String grp = it.isRoot() ? it.rootName : it.tool.getMenuGroup();
            if (!StringUtils.isEmpty(grp) && !dups.contains(grp)) {
                lst.add(grp);
                dups.add(grp);
            }
        }
        return lst;
    }

    private void updateTree(int selIdx) {
        treeViewer.setInput(editedSdk);
        treeViewer.expandAll();
        selectModelItem(selIdx);
    }
    // ----------------- ------------- Tree model processing -----------------------------------

    private void sdkToTreeModel(Sdk sdk) {
        treeModel = new ArrayList<ModelItem>();
        List<SdkTool> tools = sdk.getTools();
        String group = ""; //$NON-NLS-1$
        for (SdkTool t : tools) {
            String tgrp = t.getMenuGroup();
            if (StringUtils.isBlank(tgrp) || tgrp.equals(group)) {
                treeModel.add(new ModelItem(t));
            } else {
                treeModel.add(new ModelItem(tgrp));
                treeModel.add(new ModelItem(t));
            }
            group = tgrp;
        }

    }

    /**
     * For model like {* * R tr tr tr * * *} and root R (with 'tr' elements) or for one of 'tr' 
     * returns [2, 5] (root index, last child index)
     * 
     * Or null when no zone found
     */
    private int[] getRootZone(ModelItem it) {
        int beg = -1;
        String grp = ""; //$NON-NLS-1$
        if (it.isRoot()) {
            grp = it.rootName;
            beg = treeModel.indexOf(it);
        } else {
            grp = it.tool.getMenuGroup();
            beg = getRootIdxFor(it);
        }
        if (beg >= 0) {
            int end = beg;
            for (; end+1 < treeModel.size(); ++end) {
                ModelItem it2 = treeModel.get(end+1);
                if (it2.isRoot() || !grp.equals(it2.tool.getMenuGroup())) {
                    break;
                }
            }
            return new int[]{beg, end};
        }
        return null;
    }


    /**
     * If 'it' is tool and is child - returns its root index in the treeModer. In other case returns -1
     */
    private int getRootIdxFor(ModelItem it) {
        if (!it.isRoot() && !StringUtils.isBlank(it.tool.getMenuGroup())) {
            for (int idx = treeModel.indexOf(it); idx >= 0; --idx) {
                if (treeModel.get(idx).isRoot()) {
                    return idx;
                }
            }
        }
        return -1;
    }

    /**
     * Apply changes (if any) in internal tools model to SDK
     * NOTE: some tools in SDK may be changed at this moment: Edit tools, changing groups etc may 
     * affect to SDK tools immediately, but tools reordering, separators etc should be copyed to SDK here.  
     */
    public void applyChangesToSdk() {
        ArrayList<ModelItem> modelCp = new ArrayList<SdkToolsControl.ModelItem>();
        modelCp.addAll(treeModel);

        // Suppress extra separators:
        rescan: while (true) {
            int i;
            for (i=0; i<modelCp.size(); ++i) {
                ModelItem it = modelCp.get(i);
                if (!it.isRoot() && it.tool.isSeparator()) {
                    if (i == 0 || i == modelCp.size()-1) { // 1st or last in menu
                        break;
                    }
                    String grp = it.tool.getMenuGroup();
                    ModelItem itL = modelCp.get(i-1);
                    ModelItem itR = modelCp.get(i+1);
                    if (itR.tool != null && itR.tool.isSeparator()) { // duplicated separator
                        break;
                    }
                    if (itL.isRoot()) { // 1st inside root 
                        break;
                    }
                    if (!grp.isEmpty() && (itR.isRoot() || !grp.equals(itR.tool.getMenuGroup()))) { // last inside root
                        break;
                    }
                }
            }
            if (i < modelCp.size()) {
                modelCp.remove(i);
            } else {
                break rescan;
            }
        }

        // Re-create Sdk's tool list:
        editedSdk.removeAllTools();
        for (ModelItem it : modelCp) {
            if (!it.isRoot()) {
                editedSdk.addTool(it.tool);
            }
        }
    }


    private static class ToolCellLabelProvider extends CellLabelProvider {
        @Override
        public void update(ViewerCell cell) {
            String txt = ""; //$NON-NLS-1$
            Image img = null;
            ModelItem it = (ModelItem)cell.getElement();
            if (it.isRoot()) {
                if (cell.getColumnIndex() == 0) {
                    txt = it.rootName; 
                    img = ImageUtils.getImage(ImageUtils.PACKAGE_FRAGMENT_IMAGE_NAME);
                }
            } else {
                if (it.tool.isSeparator()) {
                    if (cell.getColumnIndex() == 0) {
                        txt = Messages.SdkToolsControl_SeparatorLine; 
                    }

                } else {
                    if (cell.getColumnIndex() == 0) {
                        txt = it.tool.getToolName();
                        if (!it.tool.isValid()) {
                            img = ImageUtils.getImage(ImageUtils.ERROR_16x16);
                        }
                    } else {
                        txt = it.tool.getLocation();
                    }
                }
            }
            cell.setText(txt);
            cell.setImage(img);
        }
    }

    private class ToolListContentProvider implements ITreeContentProvider {

        @Override
        public void dispose() {
        }

        @Override
        public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        }

        @Override
        public Object[] getElements(Object inputElement) {
            ArrayList<ModelItem> topLevel = new ArrayList<ModelItem>();
            for (ModelItem it : treeModel) {
                if (it.isRoot() || StringUtils.isEmpty(it.tool.getMenuGroup())) {
                    topLevel.add(it);
                }
            }
            return topLevel.toArray();
        }

        @Override
        public Object[] getChildren(Object parentElement) {
            int[] rz = getRootZone((ModelItem)parentElement);
            if (rz != null) {
                return treeModel.subList(rz[0]+1, rz[1]+1).toArray();
            }
            return null;
        }

        @Override
        public Object getParent(Object element) {
            int ridx = getRootIdxFor((ModelItem)element);
            if (ridx >= 0) {
                return treeModel.get(ridx);
            }
            return null;
        }

        @Override
        public boolean hasChildren(Object element) {
            return ((ModelItem)element).isRoot();
        }
    }

    private class ModelItem {
        private final SdkTool tool;
        private String rootName;


        public ModelItem(SdkTool t) {
            this.tool = t;
            this.rootName = null;
        }

        public ModelItem(String rootName) {
            tool = null;
            this.rootName = rootName;
        }

        public boolean isRoot() {
            return tool == null;
        }

        @Override
        public String toString() {
            return isRoot() ? rootName : String.valueOf(tool);
        }
    }

}