/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.csl.spi;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.EventObject;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.Caret;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.StyledDocument;
import org.netbeans.api.editor.EditorRegistry;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.lib.editor.util.swing.DocumentUtilities;
import org.netbeans.modules.csl.api.DataLoadersBridge;
import org.netbeans.modules.editor.NbEditorUtilities;
import org.netbeans.modules.editor.indent.api.IndentUtils;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.spi.CursorMovedSchedulerEvent;
import org.openide.ErrorManager;
import org.openide.cookies.EditorCookie;
import org.openide.cookies.LineCookie;
import org.openide.cookies.OpenCookie;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.loaders.DataObject;
import org.openide.loaders.DataObjectNotFoundException;
import org.openide.text.CloneableEditorSupport;
import org.openide.text.Line;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.UserQuestionException;

/**
 * Misc utilities to avoid code duplication among the various language plugins
 *
 * @author Tor Norbye
 */
public final class GsfUtilities {
    private static final Logger LOG = Logger.getLogger(GsfUtilities.class.getName());

    private GsfUtilities() { // Utility class only, no instances
    }

    public static int getLineIndent(BaseDocument doc, int offset) {
        try {
            return IndentUtils.lineIndent(doc, Utilities.getRowStart(doc, offset));
        } catch (BadLocationException ble) {
            LOG.log(Level.WARNING, null, ble);
            return 0;
        }
    }

    /**
     * Adjust the indentation of the line containing the given offset to the provided
     * indentation, and return the length difference of old and new indentation.
     *
     * Copied from Indent module's "modifyIndent"
     */
    public static int setLineIndentation(BaseDocument doc, int lineOffset, int newIndent) throws BadLocationException {
        int lineStartOffset = Utilities.getRowStart(doc, lineOffset);

        // Determine old indent first together with oldIndentEndOffset
        int indent = 0;
        int tabSize = -1;
        CharSequence docText = DocumentUtilities.getText(doc);
        int oldIndentEndOffset = lineStartOffset;
        while (oldIndentEndOffset < docText.length()) {
            char ch = docText.charAt(oldIndentEndOffset);
            if (ch == '\n') {
                break;
            } else if (ch == '\t') {
                if (tabSize == -1) {
                    tabSize = IndentUtils.tabSize(doc);
                }
                // Round to next tab stop
                indent = (indent + tabSize) / tabSize * tabSize;
            } else if (Character.isWhitespace(ch)) {
                indent++;
            } else { // non-whitespace
                break;
            }
            oldIndentEndOffset++;
        }

        String newIndentString = IndentUtils.createIndentString(doc, newIndent);
        // Attempt to match the begining characters
        int offset = lineStartOffset;
        boolean different = false;
        int i = 0;
        for (; i < newIndentString.length() && lineStartOffset + i < oldIndentEndOffset; i++) {
            if (newIndentString.charAt(i) != docText.charAt(lineStartOffset + i)) {
                offset = lineStartOffset + i;
                newIndentString = newIndentString.substring(i);
                different = true;
                break;
            }
        }
        if (!different) {
            offset = lineStartOffset + i;
            newIndentString = newIndentString.substring(i);
        }

        // Replace the old indent
        if (offset < oldIndentEndOffset) {
            doc.remove(offset, oldIndentEndOffset - offset);
        }
        if (newIndentString.length() > 0) {
            doc.insertString(offset, newIndentString, null);
        }
        return newIndentString.length() - (oldIndentEndOffset - offset);
    }


    public static JTextComponent getOpenPane() {
        JTextComponent pane = EditorRegistry.lastFocusedComponent();

        return pane;
    }

    public static JTextComponent getPaneFor(FileObject fo) {
        JTextComponent pane = getOpenPane();
        if (pane != null && findFileObject(pane) == fo) {
            return pane;
        }

        for (JTextComponent c : EditorRegistry.componentList()) {
            if (findFileObject(c) == fo) {
                return c;
            }
        }

        return null;
    }

    public static BaseDocument getDocument(FileObject fileObject, boolean openIfNecessary) {
        return getDocument(fileObject, openIfNecessary, false);
    }

    /**
     * Load the document for the given fileObject.
     * @param fileObject the file whose document we want to obtain
     * @param openIfNecessary If true, block if necessary to open the document. If false, will only return the
     *    document if it is already open.
     * @param skipLarge If true, check the file size, and if the file is really large (defined by
     *    openide.loaders), then skip it (otherwise we could end up with a large file warning).
     * @return
     */
    public static BaseDocument getDocument(FileObject fileObject, boolean openIfNecessary, boolean skipLarge) {
        if (skipLarge) {
            // Make sure we're not dealing with a huge file!
            // Causes issues like 132306
            // openide.loaders/src/org/openide/text/DataEditorSupport.java
            // has an Env#inputStream method which posts a warning to the user
            // if the file is greater than 1Mb...
            //SG_ObjectIsTooBig=The file {1} seems to be too large ({2,choice,0#{2}b|1024#{3} Kb|1100000#{4} Mb|1100000000#{5} Gb}) to safely open. \n\
            //  Opening the file could cause OutOfMemoryError, which would make the IDE unusable. Do you really want to open it?

            // Apparently there is a way to handle this
            // (see issue http://www.netbeans.org/issues/show_bug.cgi?id=148702 )
            // but for many cases, the user probably doesn't want really large files as indicated
            // by the skipLarge parameter).
            if (fileObject.getSize () > 1024 * 1024) {
                return null;
            }
        }

        try {
            EditorCookie ec = fileObject.isValid() ? DataLoadersBridge.getDefault().getCookie(fileObject, EditorCookie.class) : null;
            if (ec != null) {
                if (openIfNecessary) {
                    try {
                        return (BaseDocument) ec.openDocument();
                    } catch (UserQuestionException uqe) {
                        uqe.confirmed();
                        return (BaseDocument) ec.openDocument();
                    }
                } else {
                    return (BaseDocument) ec.getDocument();
                }
            }
        } catch (IOException ex) {
            LOG.log(Level.WARNING, null, ex);
        }

        return null;
    }

    @Deprecated // Use getDocument instead
    public static BaseDocument getBaseDocument(FileObject fileObject, boolean forceOpen) {
        return getDocument(fileObject, forceOpen);
    }

    public static FileObject findFileObject(Document doc) {
        DataObject dobj = (DataObject)doc.getProperty(Document.StreamDescriptionProperty);

        if (dobj == null) {
            return null;
        }

        return dobj.getPrimaryFile();
    }

    public static FileObject findFileObject(JTextComponent target) {
        Document doc = target.getDocument();
        return findFileObject(doc);
    }

    // Copied from UiUtils. Shouldn't this be in a common library somewhere?
    public static boolean open(final FileObject fo, final int offset, final String search) {
        if (!SwingUtilities.isEventDispatchThread()) {
            SwingUtilities.invokeLater(new Runnable() {
                    public @Override void run() {
                        doOpen(fo, offset, search);
                    }
                });

            return true; // not exactly accurate, but....
        }

        return doOpen(fo, offset, search);
    }

    // Private methods ---------------------------------------------------------
    private static boolean doOpen(FileObject fo, int offset, String search) {
        try {
            DataObject od = DataObject.find(fo);
            EditorCookie ec = od.getCookie(EditorCookie.class);
            LineCookie lc = od.getCookie(LineCookie.class);

            // If the caller hasn't specified an offset, and the document is
            // already open, don't jump to a particular line!
            if (ec != null && offset == -1 && ec.getDocument() != null && search == null) {
                ec.open();
                return true;
            }

            // Simple text search if no known offset (e.g. broken/unparseable source)
            if ((search != null) && (offset == -1)) {
                StyledDocument doc = NbDocument.getDocument(od);

                try {
                    String text = doc.getText(0, doc.getLength());
                    int caretDelta = search.indexOf('^');
                    if (caretDelta != -1) {
                        search = search.substring(0, caretDelta) + search.substring(caretDelta+1);
                    } else {
                        caretDelta = 0;
                    }
                    offset = text.indexOf(search);
                    if (offset != -1) {
                        offset += caretDelta;
                    }
                } catch (BadLocationException ble) {
                    LOG.log(Level.WARNING, null, ble);
                }
            }
            
            return NbDocument.openDocument(od, offset, Line.ShowOpenType.OPEN, Line.ShowVisibilityType.FOCUS);
        } catch (IOException e) {
            ErrorManager.getDefault().notify(ErrorManager.INFORMATIONAL, e);
        }

        return false;
    }

    public static void extractZip(final FileObject extract, final FileObject dest) throws IOException {
        File extractFile = FileUtil.toFile(extract);
        extractZip(dest, new BufferedInputStream(new FileInputStream(extractFile)));
    }

    // Based on openide/fs' FileUtil.extractJar
    private static void extractZip(final FileObject fo, final InputStream is)
    throws IOException {
        FileSystem fs = fo.getFileSystem();

        fs.runAtomicAction(
            new FileSystem.AtomicAction() {
                public @Override void run() throws IOException {
                    extractZipImpl(fo, is);
                }
            }
        );
    }

    /** Does the actual extraction of the Jar file.
     */
    // Based on openide/fs' FileUtil.extractJarImpl
    private static void extractZipImpl(FileObject fo, InputStream is)
    throws IOException {
        ZipEntry je;

        ZipInputStream jis = new ZipInputStream(is);

        while ((je = jis.getNextEntry()) != null) {
            String name = je.getName();

            if (name.toLowerCase().startsWith("meta-inf/")) {
                continue; // NOI18N
            }

            if (je.isDirectory()) {
                FileUtil.createFolder(fo, name);

                continue;
            }

            // copy the file
            FileObject fd = FileUtil.createData(fo, name);
            FileLock lock = fd.lock();

            try {
                OutputStream os = fd.getOutputStream(lock);

                try {
                    FileUtil.copy(jis, os);
                } finally {
                    os.close();
                }
            } finally {
                lock.releaseLock();
            }
        }
    }

    /** Return true iff we're editing code templates */
    public static boolean isCodeTemplateEditing(Document doc) {
        // Copied from editor/codetemplates/src/org/netbeans/lib/editor/codetemplates/CodeTemplateInsertHandler.java
        String EDITING_TEMPLATE_DOC_PROPERTY = "processing-code-template"; // NOI18N
        String CT_HANDLER_DOC_PROPERTY = "code-template-insert-handler"; // NOI18N

        return doc.getProperty(EDITING_TEMPLATE_DOC_PROPERTY) == Boolean.TRUE ||
                doc.getProperty(CT_HANDLER_DOC_PROPERTY) != null;
    }

    public static boolean isRowWhite(CharSequence text, int offset) throws BadLocationException {
        try {
            // Search forwards
            for (int i = offset; i < text.length(); i++) {
                char c = text.charAt(i);
                if (c == '\n') {
                    break;
                }
                if (!Character.isWhitespace(c)) {
                    return false;
                }
            }
            // Search backwards
            for (int i = offset-1; i >= 0; i--) {
                char c = text.charAt(i);
                if (c == '\n') {
                    break;
                }
                if (!Character.isWhitespace(c)) {
                    return false;
                }
            }

            return true;
        } catch (Exception ex) {
            BadLocationException ble = new BadLocationException(offset + " out of " + text.length(), offset);
            ble.initCause(ex);
            throw ble;
        }
    }

    public static boolean isRowEmpty(CharSequence text, int offset) throws BadLocationException {
        try {
            if (offset < text.length()) {
                char c = text.charAt(offset);
                if (!(c == '\n' || (c == '\r' && (offset == text.length()-1 || text.charAt(offset+1) == '\n')))) {
                    return false;
                }
            }

            if (!(offset == 0 || text.charAt(offset-1) == '\n')) {
                // There's previous stuff on this line
                return false;
            }

            return true;
        } catch (Exception ex) {
            BadLocationException ble = new BadLocationException(offset + " out of " + text.length(), offset);
            ble.initCause(ex);
            throw ble;
        }
    }

    public static int getRowLastNonWhite(CharSequence text, int offset) throws BadLocationException {
        try {
            // Find end of line
            int i = offset;
            for (; i < text.length(); i++) {
                char c = text.charAt(i);
                if (c == '\n' || (c == '\r' && (i == text.length()-1 || text.charAt(i+1) == '\n'))) {
                    break;
                }
            }
            // Search backwards to find last nonspace char from offset
            for (i--; i >= 0; i--) {
                char c = text.charAt(i);
                if (c == '\n') {
                    return -1;
                }
                if (!Character.isWhitespace(c)) {
                    return i;
                }
            }

            return -1;
        } catch (Exception ex) {
            BadLocationException ble = new BadLocationException(offset + " out of " + text.length(), offset);
            ble.initCause(ex);
            throw ble;
        }
    }

    public static int getRowFirstNonWhite(CharSequence text, int offset) throws BadLocationException {
        try {
            // Find start of line
            int i = offset-1;
            if (i < text.length()) {
                for (; i >= 0; i--) {
                    char c = text.charAt(i);
                    if (c == '\n') {
                        break;
                    }
                }
                i++;
            }
            // Search forwards to find first nonspace char from offset
            for (; i < text.length(); i++) {
                char c = text.charAt(i);
                if (c == '\n') {
                    return -1;
                }
                if (!Character.isWhitespace(c)) {
                    return i;
                }
            }

            return -1;
        } catch (Exception ex) {
            BadLocationException ble = new BadLocationException(offset + " out of " + text.length(), offset);
            ble.initCause(ex);
            throw ble;
        }
    }

    public static int getRowStart(CharSequence text, int offset) throws BadLocationException {
        try {
            // Search backwards
            for (int i = offset-1; i >= 0; i--) {
                char c = text.charAt(i);
                if (c == '\n') {
                    return i+1;
                }
            }

            return 0;
        } catch (Exception ex) {
            BadLocationException ble = new BadLocationException(offset + " out of " + text.length(), offset);
            ble.initCause(ex);
            throw ble;
        }
    }

    public static int getRowEnd(CharSequence text, int offset) throws BadLocationException {
        try {
            // Search backwards
            for (int i = offset; i < text.length(); i++) {
                char c = text.charAt(i);
                if (c == '\n') {
                    return i;
                }
            }

            return text.length();
        } catch (Exception ex) {
            BadLocationException ble = new BadLocationException(offset + " out of " + text.length(), offset);
            ble.initCause(ex);
            throw ble;
        }
    }

    public static boolean endsWith(StringBuilder sb, String s) {
        int len = s.length();

        if (sb.length() < len) {
            return false;
        }

        for (int i = sb.length()-len, j = 0; j < len; i++, j++) {
            if (sb.charAt(i) != s.charAt(j)) {
                return false;
            }
        }

        return true;
    }

    public static String truncate(String s, int length) {
        assert length > 3; // Not for short strings
        if (s.length() <= length) {
            return s;
        } else {
            return s.substring(0, length-3) + "...";
        }
    }

    /**
     * Gets the last known offset of the editor caret.
     *
     * @param snapshot The snapshot to get the offset for.
     * @param event The event that can contain offset information. Can be <code>null</code>.
     *
     * @return The last know caret offset or -1.
     */
    public static int getLastKnownCaretOffset(Snapshot snapshot, EventObject event) {
        // Try scheduler event first
        if (event instanceof CursorMovedSchedulerEvent) {
            return ((CursorMovedSchedulerEvent) event).getCaretOffset();
        }
        
        // defect #221922: getDocument() false forces DObj construction, which I'd like to avoid
        // during parsing. Since != -1 is only returned iff opened Editor component is found, 
        // we can as well search in the opposite direction, starting from opened editors.
        FileObject snapshotFile = snapshot.getSource().getFileObject();
        Document snapshotDoc = null;
        
        if (snapshotFile != null) {
            for(JTextComponent jtc : EditorRegistry.componentList()) {
                if (snapshotFile == NbEditorUtilities.getFileObject(jtc.getDocument())) {
                    
                    // double check: check the document is the same:
                    snapshotDoc = snapshot.getSource().getDocument(false);
                    if (snapshotDoc == null || snapshotDoc == jtc.getDocument()) {
                        Caret c = jtc.getCaret();
                        if (c != null) {
                            return c.getDot();
                        }
                    }
                    break;
                }
            }
        }
        // if the file was NOT null, the document is not opened in any editor (result of the previous search)
        // so in that case we leave snapshotDoc null and avoid creation of DObj.
        if (snapshotDoc == null && snapshotFile == null) {
            snapshotDoc = snapshot.getSource().getDocument(false);
        }
        if (snapshotDoc != null) {
            for(JTextComponent jtc : EditorRegistry.componentList()) {
                if (snapshotDoc == jtc.getDocument()) {
                    Caret c = jtc.getCaret();
                    if (c != null) {
                        return c.getDot();
                    }
                }
            }
        }        

        // Finally, try the enforced caret offset (eg. enforced by tests)
        Integer enforcedCaretOffset = enforcedCaretOffsets.get(snapshot.getSource());
        if (enforcedCaretOffset != null) {
            return enforcedCaretOffset;
        }

        return -1;
    }

    /**
     * Gets the CloneableEditorSupport for given FileObject
     *
     * @param fo A FileObject to get the CES for.
     * @return Instance of CloneableEditorSupport
     */
    public static CloneableEditorSupport findCloneableEditorSupport(FileObject fo) {
	try {
	    DataObject dob = DataObject.find(fo);
	    Object obj = dob.getCookie(OpenCookie.class);
	    if (obj instanceof CloneableEditorSupport) {
		return (CloneableEditorSupport)obj;
	    }
	    obj = dob.getCookie(org.openide.cookies.EditorCookie.class);
	    if (obj instanceof CloneableEditorSupport) {
		return (CloneableEditorSupport)obj;
	    }
	} catch (DataObjectNotFoundException ex) {
	    Exceptions.printStackTrace(ex);
	}
        return null;
    }

    // this is called from tests
    /* package */ static void setLastKnowCaretOffset(Source source, int offset) {
        enforcedCaretOffsets.put(source, offset);
    }

    private static final Map<Source, Integer> enforcedCaretOffsets = new WeakHashMap<Source, Integer>();

}
