/*
 * 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.beans.beaninfo;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.Externalizable;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.StyledDocument;
import org.netbeans.api.editor.guards.GuardedSectionManager;
import org.netbeans.api.queries.FileEncodingQuery;
import org.netbeans.core.api.multiview.MultiViews;
import org.netbeans.core.spi.multiview.CloseOperationState;
import org.netbeans.core.spi.multiview.MultiViewElement;
import org.netbeans.core.spi.multiview.MultiViewElementCallback;
import org.netbeans.core.spi.multiview.MultiViewFactory;
import org.netbeans.modules.beans.beaninfo.GenerateBeanInfoAction.BeanInfoWorker;
import org.netbeans.spi.editor.guards.GuardedEditorSupport;
import org.netbeans.spi.editor.guards.GuardedSectionsFactory;
import org.netbeans.spi.editor.guards.GuardedSectionsProvider;
import org.netbeans.spi.java.project.support.ui.templates.JavaTemplates;
import org.openide.cookies.EditCookie;
import org.openide.cookies.EditorCookie;
import org.openide.cookies.OpenCookie;
import org.openide.cookies.PrintCookie;
import org.openide.cookies.SaveCookie;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileStatusEvent;
import org.openide.filesystems.FileStatusListener;
import org.openide.filesystems.FileSystem;
import org.openide.loaders.DataObject;
import org.openide.loaders.MultiDataObject;
import org.openide.nodes.CookieSet;
import org.openide.nodes.Node;
import org.openide.text.CloneableEditor;
import org.openide.text.CloneableEditorSupport.Pane;
import org.openide.text.DataEditorSupport;
import org.openide.text.NbDocument;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.NbCollections;
import org.openide.util.Task;
import org.openide.util.TaskListener;
import org.openide.windows.CloneableOpenSupport;
import org.openide.windows.CloneableTopComponent;
import org.openide.windows.TopComponent;

/**
 *
 * @author Jan Pokorsky
 */
public final class BIEditorSupport extends DataEditorSupport
        implements OpenCookie, EditCookie, EditorCookie, PrintCookie, EditorCookie.Observable {
    
    private static final String MV_JAVA_ID = "java"; // NOI18N
    private static final String MV_BEANINFO_ID = "beaninfo"; // NOI18N
    private static final String MIME_BEAN_INFO = "text/x-java-beaninfo"; //NOI18N
    private BIGES guardedEditor;
    private GuardedSectionsProvider guardedProvider;
    private GenerateBeanInfoAction.BeanInfoWorker worker;

    private static final Set<BIEditorSupport> opened = Collections.synchronizedSet(new HashSet<BIEditorSupport>());
    private static Map<FileSystem,FileStatusListener> fsToStatusListener = new HashMap<FileSystem,FileStatusListener>();

    /**
     * The embracing multiview TopComponent (holds the form designer and
     * java editor) - we remeber the last active TopComponent (not all clones)
     */
    private CloneableTopComponent multiviewTC;
    private TopComponentsListener topComponentsListener;

    public BIEditorSupport(DataObject obj, CookieSet cookieSet) {
        super(obj, new Environment(obj, cookieSet));
        setMIMEType("text/x-java"); // NOI18N
    }
    
    public GuardedSectionManager getGuardedSectionManager() {
        try {
            StyledDocument doc = openDocument();
            return GuardedSectionManager.getInstance(doc);
        } catch (IOException ex) {
            throw (IllegalStateException) new IllegalStateException("cannot open document").initCause(ex); // NOI18N
        }
    }
    
    @Override
    protected void loadFromStreamToKit(StyledDocument doc, InputStream stream, EditorKit kit)
            throws IOException, BadLocationException {
        
        if (guardedEditor == null) {
            guardedEditor = new BIGES();
            GuardedSectionsFactory gFactory = GuardedSectionsFactory.find(((DataEditorSupport.Env) env).getMimeType());
            if (gFactory != null) {
                guardedProvider = gFactory.create(guardedEditor);
            }
        }
        
        if (guardedProvider != null) {
            guardedEditor.doc = doc;
            Charset c = FileEncodingQuery.getEncoding(this.getDataObject().getPrimaryFile());
            Reader reader = guardedProvider.createGuardedReader(stream, c);
            try {
                kit.read(reader, doc, 0);
            } finally {
                reader.close();
            }
        } else {
            super.loadFromStreamToKit(doc, stream, kit);
        }
    }

    @Override
    protected void saveFromKitToStream(StyledDocument doc, EditorKit kit, OutputStream stream)
            throws IOException, BadLocationException {
        
        if (guardedProvider != null) {
            Charset c = FileEncodingQuery.getEncoding(this.getDataObject().getPrimaryFile());
            Writer writer = guardedProvider.createGuardedWriter(stream, c);
            try {
                kit.write(writer, doc, 0, doc.getLength());
            } finally {
                writer.close();
            }
        } else {
            super.saveFromKitToStream(doc, kit, stream);
        }
    }

    @Override
    public void saveDocument() throws IOException {
        if (worker != null && worker.isModelModified()) {
            worker.generateSources();
            worker.waitFinished();
        }
        super.saveDocument();
    }
    
    @Override
    protected boolean notifyModified() {
        if (!super.notifyModified())
            return false;
        ((Environment)this.env).addSaveCookie();
        updateMVTCName();
        return true;
    }


    @Override
    protected void notifyUnmodified() {
        super.notifyUnmodified();
        ((Environment)this.env).removeSaveCookie();
        updateMVTCName();
    }

    @Override
    protected void notifyClosed() {
        opened.remove(this);
        if (opened.isEmpty()) {
            detachStatusListeners();
        }
        super.notifyClosed();
        worker = null;
        if (topComponentsListener != null) {
            TopComponent.getRegistry().removePropertyChangeListener(topComponentsListener);
            topComponentsListener = null;
        }
    }

    @Override
    protected Pane createPane() {
        DataObject dobj = getDataObject();
        if (dobj == null || !dobj.isValid()) {
            return super.createPane();
        }
        return (Pane) MultiViews.createCloneableMultiView(MIME_BEAN_INFO, getDataObject());
    }
    
    /** This is called by the multiview elements whenever they are created
     * (and given a observer knowing their multiview TopComponent). It is
     * important during deserialization and clonig the multiview - i.e. during
     * the operations we have no control over. But anytime a multiview is
     * created, this method gets called.
     */
    private void setTopComponent(TopComponent topComp) {
        multiviewTC = (CloneableTopComponent)topComp;
        updateMVTCName();

        if (topComponentsListener == null) {
            topComponentsListener = new TopComponentsListener();
            TopComponent.getRegistry().addPropertyChangeListener(topComponentsListener);
        }
        opened.add(this);
        try {
            addStatusListener(getDataObject().getPrimaryFile().getFileSystem());
        } catch (FileStateInvalidException fsiex) {
            Exceptions.printStackTrace(fsiex);
        }

    }
    
    private void addStatusListener(FileSystem fs) {
        FileStatusListener fsl = fsToStatusListener.get(fs);
        if (fsl == null) {
            fsl = new FileStatusListener() {
                @Override
                public void annotationChanged(FileStatusEvent ev) {
                    synchronized (opened) {
                        Iterator<BIEditorSupport> iter = opened.iterator();
                        while (iter.hasNext()) {
                            BIEditorSupport fes = iter.next();
                            if (ev.hasChanged(fes.getDataObject().getPrimaryFile())) {
                                fes.updateMVTCName();
                            }
                        }
                    }
                }
            };
            fs.addFileStatusListener(fsl);
            fsToStatusListener.put(fs, fsl);
        } // else do nothing - the listener is already added
    }
    
    private static void detachStatusListeners() {
        Iterator iter = fsToStatusListener.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry entry = (Map.Entry)iter.next();
            FileSystem fs = (FileSystem)entry.getKey();
            FileStatusListener fsl = (FileStatusListener)entry.getValue();
            fs.removeFileStatusListener(fsl);
        }
        fsToStatusListener.clear();
    }
    
    private void updateMVTCName() {
        Runnable task = new Runnable() {
            public void run() {
                updateMVTCNameInAwt();
            }
        };
        
        if (EventQueue.isDispatchThread()) {
            task.run();
        } else {
            EventQueue.invokeLater(task);
        }
    }
    
    private void updateMVTCNameInAwt() {
        CloneableTopComponent topComp = multiviewTC;
        if (topComp != null) {
            String htmlname = messageHtmlName();
            String name = messageName();
            String tip = messageToolTip();
            for (CloneableTopComponent o : NbCollections.
                    iterable(topComp.getReference().getComponents())) {
                
                topComp.setHtmlDisplayName(htmlname);
                topComp.setDisplayName(name);
                topComp.setName(name);
                topComp.setToolTipText(tip);
            }
        }
    }
    
    static boolean isLastView(TopComponent tc) {
        if (!(tc instanceof CloneableTopComponent))
            return false;
        
        boolean oneOrLess = true;
        Enumeration en = ((CloneableTopComponent)tc).getReference().getComponents();
        if (en.hasMoreElements()) {
            en.nextElement();
            if (en.hasMoreElements())
                oneOrLess = false;
        }
        return oneOrLess;
    }
    
    static BIEditorSupport findEditor(DataObject dobj) {
        return dobj.getLookup().lookup(BIEditorSupport.class);
    }
    
    final CloseOperationState canCloseElement(TopComponent tc) {
        // if this is not the last cloned java editor component, closing is OK
        if (!isLastView(tc)) {
            return CloseOperationState.STATE_OK;
        }

        if (!isModified()) {
            return CloseOperationState.STATE_OK;
        }

        AbstractAction save = new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    saveDocument();
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        };
        save.putValue(Action.LONG_DESCRIPTION, NbBundle.getMessage(BIEditorSupport.class, "MSG_MODIFIED", getDataObject().getPrimaryFile().getNameExt()));

        // return a placeholder state - to be sure our CloseHandler is called
        return MultiViewFactory.createUnsafeCloseState(
                "ID_BEANINFO_CLOSING", // NOI18N
                save,
                MultiViewFactory.NOOP_CLOSE_ACTION);
    }
    
    
    @MultiViewElement.Registration(displayName = "#LAB_JavaSourceView",
        iconBase = JavaTemplates.JAVA_ICON,
        persistenceType = TopComponent.PERSISTENCE_ONLY_OPENED,
        preferredID = MV_JAVA_ID,
        mimeType = MIME_BEAN_INFO,
        position = 10)
 
    public static final class JavaElement extends CloneableEditor implements MultiViewElement, Externalizable {
        
        private static final long serialVersionUID = 1L;
        private MultiViewElementCallback callback;

        public JavaElement(Lookup context) {
            super(context.lookup(DataEditorSupport.class));
            DataObject dataObject = context.lookup(DataObject.class);
            setActivatedNodes(new Node[]{dataObject.getNodeDelegate()});
        }

        /**
         * serialization stuff; do not use
         */
        private JavaElement() {
            super();
        }

        public JComponent getVisualRepresentation() {
            return this;
        }

        public JComponent getToolbarRepresentation() {
            JComponent toolbar = null;
            JEditorPane jepane = getEditorPane();
            if (jepane != null) {
                Document doc = jepane.getDocument();
                if (doc instanceof NbDocument.CustomToolbar) {
                    toolbar = ((NbDocument.CustomToolbar)doc).createToolbar(jepane);
                }
            }
            return toolbar;
        }

        public void setMultiViewCallback(MultiViewElementCallback callback) {
            this.callback = callback;
            BIEditorSupport editor = (BIEditorSupport) cloneableEditorSupport();
            editor.setTopComponent(callback.getTopComponent());
        }

        public CloseOperationState canCloseElement() {
            DataObject dataObject = callback.getTopComponent().getLookup().lookup(DataObject.class);
            BIEditorSupport editor = (BIEditorSupport) cloneableEditorSupport();
            return editor.canCloseElement(callback.getTopComponent());
        }

        @Override
        public void componentActivated() {
            super.componentActivated();
            BIEditorSupport editor = (BIEditorSupport) cloneableEditorSupport();
            if (editor.worker != null && editor.worker.isModelModified()) {
                editor.worker.generateSources();
            }
        }

        @Override
        public void componentDeactivated() {
            super.componentDeactivated();
        }

        @Override
        public void componentHidden() {
            super.componentHidden();
        }

        @Override
        public void componentShowing() {
            super.componentShowing();
        }

        @Override
        public void componentClosed() {
            // XXX copied from form module see issue 55818
            super.canClose(null, true);
            super.componentClosed();
        }
        
        @Override
        protected boolean closeLast() {
            return true;
        }

        @Override
        public void componentOpened() {
            super.componentOpened();
        }

        @Override
        public void updateName() {
            super.updateName();
            if (callback != null) {
                callback.updateTitle(getDisplayName());
            }
        }

        @Override
        public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            super.readExternal(in);
        }

        @Override
        public void writeExternal(ObjectOutput out) throws IOException {
            super.writeExternal(out);
        }
        
    }
            
    @MultiViewElement.Registration (
        displayName="#LAB_BeanInfoEditorView",
        iconBase=JavaTemplates.JAVA_ICON,
        persistenceType=TopComponent.PERSISTENCE_NEVER,
        preferredID=MV_BEANINFO_ID,
        mimeType=MIME_BEAN_INFO,
        position=1000
    )
    public static final class BeanInfoElement extends CloneableTopComponent implements MultiViewElement, Externalizable {
        
        private static final long serialVersionUID = 1L;
        private MultiViewElementCallback callback;
        private DataObject dataObject;
        private boolean isInitialized = false;
        private final JPanel emptyToolbar = new JPanel();
        private BiPanel biPanel;
        
        public BeanInfoElement(Lookup lookup) {
            this.dataObject = lookup.lookup(DataObject.class);
            setActivatedNodes(new Node[]{dataObject.getNodeDelegate()});
        }

        /**
         * serialization stuff; do not use
         */
        private BeanInfoElement() {
        }

        public JComponent getVisualRepresentation() {
            return this;
        }
        
        public JComponent getToolbarRepresentation() {
            return emptyToolbar;
        }

        public void setMultiViewCallback(MultiViewElementCallback callback) {
            this.callback = callback;
            BIEditorSupport editor = findEditor(dataObject);
            editor.setTopComponent(callback.getTopComponent());
        }

        @Override
        public CloseOperationState canCloseElement() {
            BIEditorSupport editor = findEditor(dataObject);
            return editor.canCloseElement(callback.getTopComponent());
        }

        @Override
        public void componentActivated() {
            super.componentActivated();
        }

        @Override
        public void componentDeactivated() {
            super.componentDeactivated();
        }

        @Override
        public void componentHidden() {
            super.componentHidden();
        }

        @Override
        public void componentShowing() {
            super.componentShowing();
            initialize();
        }

        @Override
        public void componentClosed() {
            super.componentClosed();
        }

        @Override
        public void componentOpened() {
            super.componentOpened();
        }
        
        private void initialize() {
            if (!isInitialized) {
                setLayout(new BorderLayout());
                biPanel = new BiPanel();
                add(biPanel, BorderLayout.CENTER);
                isInitialized = true;
            } else {
                biPanel.setContext(new BiNode.Wait());
            }
            
            FileObject biFile = dataObject.getPrimaryFile();
            String name = biFile.getName();
            name = name.substring(0, name.length() - "BeanInfo".length()); // NOI18N
            FileObject javaFile = biFile.getParent().getFileObject(name, biFile.getExt());
            BIEditorSupport editor = findEditor(dataObject);
            if (javaFile != null) {
                final BeanInfoWorker beanInfoWorker = new GenerateBeanInfoAction.BeanInfoWorker(javaFile, biPanel);
                editor.worker = beanInfoWorker;
                beanInfoWorker.analyzePatterns().addTaskListener(new TaskListener() {

                    public void taskFinished(Task task) {
                        beanInfoWorker.updateUI();
                    }
                });
            } else {
                // notify missing source file
                biPanel.setContext(BiNode.createNoSourceNode(biFile));
            }
        }

        @Override
        public void readExternal(ObjectInput oi) throws IOException, ClassNotFoundException {
            super.readExternal(oi);
            dataObject = (DataObject) oi.readObject();
//            initialize();
        }

        @Override
        public void writeExternal(ObjectOutput oo) throws IOException {
            super.writeExternal(oo);
            oo.writeObject(dataObject);
        }

    }

    private static final class BIGES implements GuardedEditorSupport {
        
        StyledDocument doc = null;
        
        public StyledDocument getDocument() {
            return BIGES.this.doc;
        }
    }
    
    private static final class Environment extends DataEditorSupport.Env {

        private static final long serialVersionUID = -1;

        private final transient CookieSet cookieSet;
        private transient SaveSupport saveCookie = null;

        private final class SaveSupport implements SaveCookie {
            public void save() throws java.io.IOException {
                DataObject dobj = getDataObject();
                ((DataEditorSupport) findCloneableOpenSupport()).saveDocument();
                dobj.setModified(false);
            }
        }

        public Environment(DataObject obj, CookieSet cookieSet) {
            super(obj);
            this.cookieSet = cookieSet;
        }

        protected FileObject getFile() {
            return this.getDataObject().getPrimaryFile();
        }

        protected FileLock takeLock() throws java.io.IOException {
            return ((MultiDataObject)this.getDataObject()).getPrimaryEntry().takeLock();
        }

        public @Override CloneableOpenSupport findCloneableOpenSupport() {
            return findEditor(this.getDataObject());
        }


        public void addSaveCookie() {
            DataObject javaData = this.getDataObject();
            if (javaData.getCookie(SaveCookie.class) == null) {
                if (this.saveCookie == null)
                    this.saveCookie = new SaveSupport();
                this.cookieSet.add(this.saveCookie);
                javaData.setModified(true);
            }
        }

        public void removeSaveCookie() {
            DataObject javaData = this.getDataObject();
            if (javaData.getCookie(SaveCookie.class) != null) {
                this.cookieSet.remove(this.saveCookie);
                javaData.setModified(false);
            }
        }
    }
    
    private class TopComponentsListener implements PropertyChangeListener {
        
        public void propertyChange(PropertyChangeEvent evt) {
            if (TopComponent.Registry.PROP_OPENED.equals(evt.getPropertyName())) {
                // Check closed top components
                @SuppressWarnings("unchecked")
                Set<TopComponent> closed = (Set<TopComponent>) evt.getOldValue();
                closed.removeAll((Set) evt.getNewValue());
                for (TopComponent o : closed) {
                    if (o instanceof CloneableTopComponent) {
                        final CloneableTopComponent topComponent = (CloneableTopComponent) o;
                        Enumeration en = topComponent.getReference().getComponents();
                        if (multiviewTC == topComponent) {
                            if (en.hasMoreElements()) {
                                // Remember next cloned top component
                                multiviewTC = (CloneableTopComponent) en.nextElement();
                            } else {
                                // All cloned top components are closed
                                notifyClosed();
                            }
                        }
                    }
                }
            }
        }
        
    }

}
