/**
 * Copyright (c) 2014 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Eclipse Public License (EPL).
 * Please see the license.txt included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
package org.python.pydev.editor.actions.organize_imports;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;

import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
import org.python.pydev.core.IPyFormatStdProvider;
import org.python.pydev.core.docutils.ImportHandle;
import org.python.pydev.core.docutils.ImportHandle.ImportHandleInfo;
import org.python.pydev.core.docutils.PyImportsHandling;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.actions.PyFormatStd;
import org.python.pydev.plugin.preferences.PydevPrefs;
import org.python.pydev.shared_core.SharedCorePlugin;
import org.python.pydev.shared_core.string.FastStringBuffer;
import org.python.pydev.shared_core.string.StringUtils;
import org.python.pydev.shared_core.structure.Tuple;
import org.python.pydev.shared_core.structure.Tuple3;
import org.python.pydev.ui.importsconf.ImportsPreferencesPage;

public class ImportArranger {

    public boolean addNewLinesToImports = false;

    private final class FromImportEntries {
        private final List<ImportHandleInfo> containedImports = new ArrayList<>();

        private final List<Tuple<String, String>> importsAndComments = new ArrayList<Tuple<String, String>>();
        private final List<Tuple<String, String>> importsAndNoComments = new ArrayList<Tuple<String, String>>();

        private final FastStringBuffer lastFromXXXImportWritten = new FastStringBuffer();
        private final FastStringBuffer line = new FastStringBuffer();
        private String from;

        public void add(ImportHandleInfo info) {
            containedImports.add(info);
        }

        private void checkForCommentsAfterImport() {
            //first, reorganize them in the order to be written (the ones with comments after the ones without)

            for (ImportHandleInfo v : FromImportEntries.this.containedImports) {
                List<String> importedStr = v.getImportedStr();
                List<String> commentsForImports = v.getCommentsForImports();
                for (int i = 0; i < importedStr.size(); i++) {
                    String importedString = importedStr.get(i).trim();
                    String comment = commentsForImports.get(i).trim();
                    boolean isWildImport = importedString.equals("*");
                    if (isWildImport) {
                        importsAndComments.clear();
                        importsAndNoComments.clear();
                    }
                    if (comment.length() > 0) {
                        importsAndComments.add(new Tuple<String, String>(importedString, comment));
                    } else {
                        importsAndNoComments.add(new Tuple<String, String>(importedString, comment));
                    }
                    if (isWildImport) {
                        return;
                    }
                }
            }
        }

        public void setFrom(String from) {
            this.from = from;
        }

        public void arrangeAndAdd(FastStringBuffer all) {

            // TODO: this could be clarified further but ...
            //ok, it's all filled, let's start rewriting it!
            boolean firstInLine = true;
            line.clear();
            boolean addedParenForLine = false;

            //ok, write all the ones with comments after the ones without any comments (each one with comment
            //will be written as a new import)
            importsAndNoComments.addAll(importsAndComments);

            if (sortNamesGrouped) {
                Comparator<? super Tuple<String, String>> c = new Comparator<Tuple<String, String>>() {

                    @Override
                    public int compare(Tuple<String, String> o1, Tuple<String, String> o2) {
                        return o1.o1.compareTo(o2.o1);
                    }
                };
                Collections.sort(importsAndNoComments, c);
            }

            for (int i = 0; i < importsAndNoComments.size(); i++) {

                Tuple<String, String> tuple = importsAndNoComments.get(i);

                if (firstInLine) {
                    lastFromXXXImportWritten.clear();
                    lastFromXXXImportWritten.append("from ");
                    lastFromXXXImportWritten.append(from);
                    lastFromXXXImportWritten.append(" import ");
                    line.append(lastFromXXXImportWritten);
                } else {
                    line.append(", ");
                }

                if (multilineImports) {
                    if (line.length() + tuple.o1.length() + tuple.o2.length() > maxCols) {
                        String addAfter = indentStr;
                        //we have to make the wrapping
                        if (breakWithParenthesis) {
                            if (!addedParenForLine) {
                                line.insert(lastFromXXXImportWritten.length(), '(');
                                if (StringUtils.rightTrim(line.toString()).length() > maxCols) {
                                    addAfter = indentStr
                                            + line.subSequence(lastFromXXXImportWritten.length() + 1, line.length())
                                                    .toString();
                                    line.setLength(lastFromXXXImportWritten.length() + 1);
                                }
                                addedParenForLine = true;
                            }
                            line.append(endLineDelim);
                        } else {
                            line.append('\\');
                            line.append(endLineDelim);
                        }
                        all.append(line);
                        line.clear();
                        line.append(addAfter);
                    }
                }

                line.append(tuple.o1);

                if (addedParenForLine && i == importsAndNoComments.size()) {
                    addedParenForLine = false;
                    line.append(')');
                }

                firstInLine = false;

                if (tuple.o2.length() > 0) {
                    if (addedParenForLine) {
                        addedParenForLine = false;
                        line.append(')');
                    }
                    line.append(' ');
                    line.append(tuple.o2);
                    line.append(endLineDelim);
                    all.append(line);
                    line.clear();
                    firstInLine = true;
                }
            }

            if (!firstInLine) {
                if (addedParenForLine) {
                    addedParenForLine = false;
                    line.append(')');
                }
                line.append(endLineDelim);
                all.append(line);
                line.clear();
            }
        }
    }

    /**
     * @return the maximum number of columns that may be available in a line.
     */
    private static int getMaxCols(boolean multilineImports) {
        final int maxCols;
        if (multilineImports) {
            if (SharedCorePlugin.inTestMode()) {
                maxCols = 80;
            } else {
                IPreferenceStore chainedPrefStore = PydevPrefs.getChainedPrefStore();
                maxCols = chainedPrefStore
                        .getInt(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN);
            }
        } else {
            maxCols = Integer.MAX_VALUE;
        }
        return maxCols;
    }

    /**
     * @return true if the imports should be split with parenthesis (instead of escaping)
     */
    private static boolean getBreakImportsWithParenthesis(IPyFormatStdProvider edit) {
        String breakIportMode = ImportsPreferencesPage.getBreakIportMode(edit);
        boolean breakWithParenthesis = true;
        if (!breakIportMode.equals(ImportsPreferencesPage.BREAK_IMPORTS_MODE_PARENTHESIS)) {
            breakWithParenthesis = false;
        }
        return breakWithParenthesis;
    }

    protected final IDocument doc;
    protected final String endLineDelim;
    private final String indentStr;
    private int lineForNewImports = -1;
    private final boolean multilineImports;
    private final boolean sortNamesGrouped;
    private int maxCols;
    private final boolean breakWithParenthesis;
    private final boolean removeUnusedImports;
    private final boolean automatic;
    protected final IPyFormatStdProvider edit;

    public ImportArranger(IDocument doc, boolean removeUnusedImports, String endLineDelim, String indentStr,
            boolean automatic, IPyFormatStdProvider edit) {
        this.doc = doc;
        this.endLineDelim = endLineDelim;
        this.indentStr = indentStr;
        this.removeUnusedImports = removeUnusedImports;
        this.automatic = automatic;
        this.edit = edit;
        multilineImports = ImportsPreferencesPage.getMultilineImports(edit);
        sortNamesGrouped = ImportsPreferencesPage.getSortNamesGrouped(edit);
        breakWithParenthesis = getBreakImportsWithParenthesis(edit);
        maxCols = getMaxCols(multilineImports);
    }

    public void perform() {
        perform(ImportsPreferencesPage.getGroupImports(edit), edit);
    }

    protected void perform(boolean groupFromImports, IPyFormatStdProvider edit) {
        boolean executeOnlyIfChanged = automatic;
        perform(groupFromImports, executeOnlyIfChanged, edit);
    }

    /**
     * @param executeOnlyIfChanged: if 'true' initially, we'll check if something changes first. If something changes
     * it'll call itself again with 'false' to force the changes.
     */
    private void perform(boolean groupFromImports, boolean executeOnlyIfChanged, IPyFormatStdProvider edit) {
        List<Tuple3<Integer, String, ImportHandle>> list = collectImports();
        if (list.isEmpty()) {
            return;
        }
        int lineOfFirstOldImport = list.get(0).o1;

        List<Tuple<Integer, String>> linesToDelete = deleteImports(list);
        if (!executeOnlyIfChanged) {
            for (Tuple<Integer, String> tup : linesToDelete) {
                PySelection.deleteLine(doc, tup.o1);
            }
        }

        lineForNewImports = insertImportsHere(lineOfFirstOldImport);

        if (this.removeUnusedImports) {
            pruneEmptyImports(list);
        }

        sortImports(list);

        //now, re-add the imports
        String finalStr = createImportsStr(groupFromImports, list);

        if (executeOnlyIfChanged) {
            //If going automatic, let's check the contents before actually doing the organize 
            //(and skip if the order is ok).
            ArrayList<String> list2 = new ArrayList<String>();
            for (Tuple<Integer, String> tup : linesToDelete) {
                list2.add(tup.o2);
            }
            Collections.reverse(list2);
            String join = StringUtils.join("", list2).trim();
            String other = StringUtils.replaceNewLines(finalStr, "").trim();
            if (join.equals(other)) {
                //                System.out.println("Equals");
            } else {
                //                System.out.println("Not equal!");
                //                System.out.println("\n\n---");
                //                System.out.println(join);
                //                System.out.println("---");
                //                System.out.println(other);
                //                System.out.println("---\n");
                perform(groupFromImports, false, edit);
            }
            return;
        }

        try {
            PyFormatStd std = new PyFormatStd();
            boolean throwSyntaxError = false;
            ISelectionProvider selectionProvider = null;
            int[] regionsToFormat = null;
            IDocument psDoc = new Document(finalStr);
            PySelection ps = new PySelection(psDoc);
            std.applyFormatAction(edit, ps, regionsToFormat, throwSyntaxError, selectionProvider);
            finalStr = psDoc.get();
            if (addNewLinesToImports) {
                // Leave 2 empty new lines separating imports from code 
                String expectedEnd = endLineDelim + endLineDelim + endLineDelim;
                while (!finalStr.endsWith(expectedEnd)) {
                    finalStr += endLineDelim;
                }
            }
        } catch (Exception e) {
            Log.log(e);
        }

        PySelection.addLine(doc, endLineDelim, finalStr, lineForNewImports);

    }

    private String createImportsStr(boolean groupFromImports, List<Tuple3<Integer, String, ImportHandle>> list) {
        FastStringBuffer all = new FastStringBuffer();

        if (!groupFromImports) {
            writeImports(list, all);

        } else { //we have to group the imports!

            groupAndWriteImports(list, all);
        }

        String finalStr = all.toString();
        return finalStr;
    }

    private void pruneEmptyImports(List<Tuple3<Integer, String, ImportHandle>> list) {
        Iterator<Tuple3<Integer, String, ImportHandle>> it = list.iterator();
        while (it.hasNext()) {
            ImportHandle ih = it.next().o3;
            List<ImportHandleInfo> info = ih.getImportInfo();
            Iterator<ImportHandleInfo> itInfo = info.iterator();
            while (itInfo.hasNext()) {
                if (itInfo.next().getImportedStr().isEmpty()) {
                    itInfo.remove();
                }
            }
            if (info.size() == 0) {
                it.remove();
            }
        }
    }

    protected void writeImports(List<Tuple3<Integer, String, ImportHandle>> list, FastStringBuffer all) {
        beforeImports(all);
        //no grouping
        for (Iterator<Tuple3<Integer, String, ImportHandle>> iter = list.iterator(); iter.hasNext();) {
            Tuple3<Integer, String, ImportHandle> element = iter.next();
            beforeImport(element, all);
            all.append(element.o2);
            all.append(endLineDelim);
        }
        afterImports(all);
    }

    protected void beforeImports(FastStringBuffer all) {
    }

    protected void afterImports(FastStringBuffer all) {
    }

    protected void beforeImport(Tuple3<Integer, String, ImportHandle> element, FastStringBuffer all) {
        // do nothing
    }

    protected int insertImportsHere(int lineOfFirstOldImport) {
        return lineOfFirstOldImport - 1;
    }

    private void groupAndWriteImports(List<Tuple3<Integer, String, ImportHandle>> list, FastStringBuffer all) {
        //import from to the imports that should be grouped given its 'from'
        TreeMap<String, FromImportEntries> importsWithFrom = new TreeMap<String, FromImportEntries>(
                new Comparator<String>() {

                    @Override
                    public int compare(String o1, String o2) {
                        Tuple<String, String> splitted1 = StringUtils.splitOnFirst(o1, '.');
                        Tuple<String, String> splitted2 = StringUtils.splitOnFirst(o2, '.');

                        boolean isFuture1 = splitted1.o1.equals("__future__");
                        boolean isFuture2 = splitted2.o1.equals("__future__");

                        if (isFuture1 != isFuture2) {
                            if (isFuture1) {
                                return -1;
                            }
                            return 1;
                        }

                        return o1.compareTo(o2);
                    }
                });
        List<ImportHandleInfo> importsWithoutFrom = new ArrayList<ImportHandleInfo>();

        fillImportStructures(list, importsWithFrom, importsWithoutFrom);

        Set<Entry<String, FromImportEntries>> entrySet = importsWithFrom.entrySet();

        for (Entry<String, FromImportEntries> entry : entrySet) {

            FromImportEntries value = entry.getValue();

            value.setFrom(entry.getKey());
            value.checkForCommentsAfterImport();
            value.arrangeAndAdd(all);
        }

        writeImportsWithoutFrom(all, importsWithoutFrom);
    }

    /**
     * Fills the import structure passed, so that the imports from will be grouped by the 'from' part and the regular
     * imports will be in a separate list.
     */
    private void fillImportStructures(List<Tuple3<Integer, String, ImportHandle>> list,
            TreeMap<String, FromImportEntries> importsWithFrom, List<ImportHandleInfo> importsWithoutFrom) {
        //fill the info
        for (Tuple3<Integer, String, ImportHandle> element : list) {

            List<ImportHandleInfo> importInfo = element.o3.getImportInfo();
            for (ImportHandleInfo importHandleInfo : importInfo) {
                String fromImportStr = importHandleInfo.getFromImportStrWithoutUnwantedChars();
                if (fromImportStr == null) {
                    importsWithoutFrom.add(importHandleInfo);
                } else {
                    FromImportEntries lst = importsWithFrom.get(fromImportStr);
                    if (lst == null) {
                        lst = new FromImportEntries();
                        importsWithFrom.put(fromImportStr, lst);
                    }
                    lst.add(importHandleInfo);
                }
            }
        }
    }

    protected void sortImports(List<Tuple3<Integer, String, ImportHandle>> list) {
        Collections.sort(list, new Comparator<Tuple3<Integer, String, ImportHandle>>() {

            @Override
            public int compare(Tuple3<Integer, String, ImportHandle> o1, Tuple3<Integer, String, ImportHandle> o2) {
                //When it's __future__, it has to appear before the others.
                List<ImportHandleInfo> info1 = o1.o3.getImportInfo();
                List<ImportHandleInfo> info2 = o2.o3.getImportInfo();
                boolean isFuture1 = getIsFuture(info1);
                boolean isFuture2 = getIsFuture(info2);
                if (isFuture1 && !isFuture2) {
                    return -1;
                }
                if (!isFuture1 && isFuture2) {
                    return 1;
                }
                return o1.o2.compareTo(o2.o2);
            }

            private boolean getIsFuture(List<ImportHandleInfo> info1) {
                String from1 = null;
                if (info1.size() > 0) {
                    from1 = info1.get(0).getFromImportStr();
                }
                boolean isFuture = from1 != null && from1.equals("__future__");
                return isFuture;
            }
        });
    }

    private List<Tuple<Integer, String>> deleteImports(List<Tuple3<Integer, String, ImportHandle>> list) {
        //sort in inverse order (for removal of the string of the document).
        List<Tuple<Integer, String>> linesToDelete = new ArrayList<>();
        Collections.sort(list, new Comparator<Tuple3<Integer, String, ImportHandle>>() {

            @Override
            public int compare(Tuple3<Integer, String, ImportHandle> o1, Tuple3<Integer, String, ImportHandle> o2) {
                return o2.o1.compareTo(o1.o1);
            }
        });
        //ok, now we have to delete all lines with imports.
        for (Iterator<Tuple3<Integer, String, ImportHandle>> iter = list.iterator(); iter.hasNext();) {
            Tuple3<Integer, String, ImportHandle> element = iter.next();
            String s = element.o2;
            int max = StringUtils.countLineBreaks(s);
            for (int i = 0; i <= max; i++) {
                int lineToDel = (element.o1).intValue();

                int j = lineToDel + i;
                linesToDelete.add(new Tuple(j, PySelection.getLine(doc, j)));
            }
        }
        Comparator<? super Tuple<Integer, String>> c = new Comparator<Tuple<Integer, String>>() {

            @Override
            public int compare(Tuple<Integer, String> o1, Tuple<Integer, String> o2) {
                return Integer.compare(o2.o1, o1.o1); //reversed compare (o2 first o1 last).
            }
        };
        Collections.sort(linesToDelete, c);
        return linesToDelete;
    }

    final List<Tuple3<Integer, String, ImportHandle>> collectImports() {

        List<Tuple3<Integer, String, ImportHandle>> list = new ArrayList<Tuple3<Integer, String, ImportHandle>>();
        //Gather imports in a structure we can work on.
        PyImportsHandling pyImportsHandling = new PyImportsHandling(doc, true, this.removeUnusedImports);
        for (ImportHandle imp : pyImportsHandling) {
            if (imp.importFound.contains("@NoMove")) {
                continue;
            }
            list.add(new Tuple3<Integer, String, ImportHandle>(imp.startFoundLine, imp.importFound, imp));
        }
        return list;
    }

    /**
     * Write the imports that don't have a 'from' in the beggining (regular imports)
     */
    private void writeImportsWithoutFrom(FastStringBuffer all,
            List<ImportHandleInfo> importsWithoutFrom) {
        //now, write the regular imports (no wrapping or tabbing here)
        for (ImportHandleInfo info : importsWithoutFrom) {

            List<String> importedStr = info.getImportedStr();
            List<String> commentsForImports = info.getCommentsForImports();
            for (int i = 0; i < importedStr.size(); i++) {
                all.append("import ");
                String importedString = importedStr.get(i);
                String comment = commentsForImports.get(i);
                all.append(importedString);
                if (comment.length() > 0) {
                    all.append(' ');
                    all.append(comment);
                }
                all.append(endLineDelim);
            }
        }
    }
}