/*
 * 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.j2ee.persistence.unit;

import java.awt.Image;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingUtilities;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.modules.j2ee.persistence.dd.PersistenceMetadata;
import org.netbeans.modules.j2ee.persistence.dd.common.Persistence;
import org.netbeans.modules.j2ee.persistence.dd.common.PersistenceUnit;
import org.netbeans.modules.j2ee.persistence.provider.ProviderUtil;
import org.openide.DialogDisplayer;
import org.netbeans.api.xml.cookies.CheckXMLCookie;
import org.netbeans.api.xml.cookies.ValidateXMLCookie;
import org.netbeans.core.spi.multiview.MultiViewElement;
import org.netbeans.modules.j2ee.persistence.dd.common.JPAParseUtils;
import org.netbeans.modules.schema2beans.BaseBean;
import org.netbeans.modules.schema2beans.Schema2BeansException;
import org.netbeans.modules.xml.multiview.ToolBarMultiViewElement;
import org.netbeans.modules.xml.multiview.XmlMultiViewDataObject;
import org.netbeans.modules.xml.multiview.XmlMultiViewDataSynchronizer;
import org.netbeans.modules.xml.multiview.XmlMultiViewElement;
import org.netbeans.spi.xml.cookies.CheckXMLSupport;
import org.netbeans.spi.xml.cookies.DataObjectAdapters;
import org.netbeans.spi.xml.cookies.ValidateXMLSupport;
import org.openide.NotifyDescriptor;
import org.openide.cookies.EditorCookie;
import org.openide.filesystems.FileLock;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.MIMEResolver;
import org.openide.loaders.DataObjectExistsException;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.openide.windows.TopComponent;
import org.xml.sax.SAXException;

/**
 * Multiview data object for persistence.xml.
 *
 * @author Martin Adamek
 * @author Erno Mononen
 */
@MIMEResolver.Registration(
    displayName="",
    position=220,
    resource="../ui/resources/PUResolver.xml"
)
public class PUDataObject extends XmlMultiViewDataObject {
    
    public static final String HELP_ID_DESIGN_PERSISTENCE_UNIT
            = "persistence_multiview_design_persistenceUnitNode";  // NOI18N
    private final ModelSynchronizer modelSynchronizer;
    /**
     * Update delay for model synchronizer.
     */
    public static final int UPDATE_DELAY = 200;
    private static final int TYPE_TOOLBAR = 0;
    private Persistence persistence;
    private static final String DESIGN_VIEW_ID = "persistence_multiview_design"; // NOI18N
    private static final Logger LOG = Logger.getLogger(PUDataObject.class.getName());
    public static final String NO_UI_PU_CLASSES_CHANGED = "non ui pu classes modified";  //NOI18N
    public static final String ICON = "org/netbeans/modules/j2ee/persistence/unit/PersistenceIcon.gif"; //NOI18N
    public static final String PREFERRED_ID_SOURCE="persistence_multiview_source"; //NOI18N
    public static final String PREFERRED_ID_DESIGN="persistence_multiview_design"; //NOI18N
    /**
     * The property name for the event fired when a persistence unit was added or removed.
     */ 
    static final String PERSISTENCE_UNIT_ADDED_OR_REMOVED = "persistence_unit_added_or_removed"; //NOI18N

    protected boolean changedFromUI;
    
    /**
     * Creates a new instance of PUDataObject.
     */
    public PUDataObject(FileObject pf, PUDataLoader loader) throws DataObjectExistsException {
        super(pf, loader);
        getEditorSupport().setMIMEType(PUDataLoader.REQUIRED_MIME);
        modelSynchronizer = new ModelSynchronizer(this);
        org.xml.sax.InputSource in = DataObjectAdapters.inputSource(this);
        CheckXMLCookie checkCookie = new CheckXMLSupport(in);
        getCookieSet().add(checkCookie);
        ValidateXMLCookie validateCookie = new ValidateXMLSupport(in);
        getCookieSet().add(validateCookie);
        parseDocument();
    }
    
    @Override
    protected Node createNodeDelegate() {
        return new PUDataNode(this);
    }

    @Override
    protected String getEditorMimeType() {
        return PUDataLoader.REQUIRED_MIME;
    }
    
    @Override
    protected int getXMLMultiViewIndex(){
        return 1;
    }
    
    @MultiViewElement.Registration(
        mimeType=PUDataLoader.REQUIRED_MIME,
        iconBase=ICON,
        persistenceType=TopComponent.PERSISTENCE_ONLY_OPENED,
        preferredID=PREFERRED_ID_SOURCE,
        displayName="#CTL_SourceTabCaption",
        position=2000
    )
    @Messages("CTL_SourceTabCaption=Source")
    public static XmlMultiViewElement createXmlMultiViewElement(Lookup lookup) {
        return new XmlMultiViewElement(lookup.lookup(XmlMultiViewDataObject.class));
    }

    @Override
    protected int associateLookup() {
        return 1;
    }

    /**
     * Saves the document.
     * @see EditorCookie#saveDocument
     */
    public void save(){
        EditorCookie edit = (EditorCookie) getLookup().lookup(EditorCookie.class);
        if (edit != null){
            try {
                edit.saveDocument();
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
    }
    
    /**
     * Parses the document.
     * @return true if document could be parsed (it was valid), false otwherwise.
     */
    public boolean parseDocument() {
        if (persistence==null) {
            try {
                persistence = getPersistence();
            } catch (RuntimeException ex) { // must catch RTE (thrown by schema2beans when document is not valid)
                LOG.log(Level.INFO, null, ex);
                return false;
            }
        } else if (isModified()){//if it's isn't modified and persistenc eexits (parsed) no need to reparse
            try{
                String oldVersion = persistence.getVersion();
                java.io.InputStream is = getEditorSupport().getInputStream();
                String version=Persistence.VERSION_1_0;
                try {
                    version=JPAParseUtils.getVersion(is);
                } catch (SAXException ex) {
                    LOG.log(Level.INFO, null, ex);//persistence.xml may be corrupted, but no need to show exception dialog
                }
                finally
                {
                    is.close();
                }
                is = getEditorSupport().getInputStream();
                Persistence newPersistence;
                Persistence cleanPersistence;
                try {
                    if(Persistence.VERSION_2_1.equals(version)) {
                        newPersistence = org.netbeans.modules.j2ee.persistence.dd.persistence.model_2_1.Persistence.createGraph(is);
                        cleanPersistence = new org.netbeans.modules.j2ee.persistence.dd.persistence.model_2_1.Persistence();
                    } else if(Persistence.VERSION_2_0.equals(version)) {
                        newPersistence = org.netbeans.modules.j2ee.persistence.dd.persistence.model_2_0.Persistence.createGraph(is);
                        cleanPersistence = new org.netbeans.modules.j2ee.persistence.dd.persistence.model_2_0.Persistence();
                    } else {//1.0 - default
                        newPersistence = org.netbeans.modules.j2ee.persistence.dd.persistence.model_1_0.Persistence.createGraph(is);
                        cleanPersistence = new org.netbeans.modules.j2ee.persistence.dd.persistence.model_1_0.Persistence();
                    }
                } catch (RuntimeException ex) { // must catch RTE (thrown by schema2beans when document is not valid)
                    LOG.log(Level.INFO, null, ex);
                    return false;
                }
                if (newPersistence!=null) {
                    try{
                        ((BaseBean)persistence).merge((BaseBean) cleanPersistence, BaseBean.MERGE_UPDATE);
                        ((BaseBean)persistence).merge((BaseBean) newPersistence, BaseBean.MERGE_UPDATE);
                    } catch (IllegalArgumentException iae) {
                        // see #104180
                        LOG.log(Level.FINE, "IAE thrown during merge, see #104180.", iae); //NOI18N
                        if(!oldVersion.equals(newPersistence.getVersion())){//version may be changed, just replace instead of merge then, see #173233 also
                            persistence = null;
                            PersistenceMetadata.getDefault().refresh(getPrimaryFile());
                            return true;
                        }
                        else {
                            return false;
                        }
                    }
                }
            } catch (IOException ioe){
                LOG.log(Level.INFO, null, ioe);
                return false;
            } catch (IllegalStateException e) {
                //issue 134726, sometimes faled to parser document if it's changed during update, just skip, should be parsed with next event
                LOG.log(Level.INFO, null, e);
                return false;
            }
        }
        return true;
    }
    
    
    /**
     * Checks whether the preferred view can be displayed and switches to the
     * xml view and displays an appropriate warning if not. In case that
     * the preferred view is the design view, it
     * can be displayed if <ol><li>document is valid (parseable) and</li>
     *<li>the target server is attached></li></ol>.
     *@return true if the preferred view can be displayed, false otherwise.
     */
    public boolean viewCanBeDisplayed() {
        
        boolean switchView = false;
        NotifyDescriptor nd = null;
        if(FileOwnerQuery.getOwner(getPrimaryFile())==null) {
             nd = new org.openide.NotifyDescriptor.Message(
                    NbBundle.getMessage(PUDataObject.class, "TXT_StandAlonePersistence",
                    getPrimaryFile().getNameExt()), NotifyDescriptor.WARNING_MESSAGE);
            switchView = true;           
        } else if (!parseDocument() && getSelectedPerspective().preferredID().startsWith(DESIGN_VIEW_ID)) {
            nd = new org.openide.NotifyDescriptor.Message(
                    NbBundle.getMessage(PUDataObject.class, "TXT_DocumentUnparsable",
                    getPrimaryFile().getNameExt()), NotifyDescriptor.WARNING_MESSAGE);
            switchView = true;
            
        } else if (!ProviderUtil.isValidServerInstanceOrNone(FileOwnerQuery.getOwner(getPrimaryFile()))
        && getSelectedPerspective().preferredID().startsWith(DESIGN_VIEW_ID)){
            
            nd = new org.openide.NotifyDescriptor.Message(
                    NbBundle.getMessage(PUDataObject.class, "TXT_ServerMissing"),
                    NotifyDescriptor.WARNING_MESSAGE);
            switchView = true;
        }
        
        if (switchView){
            DialogDisplayer.getDefault().notify(nd);
            // postpone the "Switch to XML View" action to the end of event dispatching thread
            // this enables to finish the current action first (e.g. painting particular view)
            // see the issue 67580
            SwingUtilities.invokeLater(new Runnable(){
                @Override
                public void run() {
                    goToXmlView();
                }
            });
        }
        return !switchView;

    }
    
    
    /**
     * Gets the object graph representing the contents of the 
     * persistence.xml deployment desciptor with which this data object 
     * is associated.
     *
     * @return the persistence graph.
     *
     */
    public Persistence getPersistence(){
        if (persistence==null) {
            try {
                persistence = PersistenceMetadata.getDefault().getRoot(getPrimaryFile());
            } catch (IOException ex) {
                Exceptions.printStackTrace(ex);
            }
        }
        assert persistence != null;
        return persistence;
    }
    
    /**
     * Adds given persistence unit and schedules update of data.
     */
    public void addPersistenceUnit(PersistenceUnit persistenceUnit){
        Project project = FileOwnerQuery.getOwner(getPrimaryFile());
        if(project != null) {
            ProviderUtil.makePortableIfPossible(project, persistenceUnit);
        }
        getPersistence().addPersistenceUnit(persistenceUnit);
        modelUpdated();
        firePropertyChange(PERSISTENCE_UNIT_ADDED_OR_REMOVED, false, true);
    }
    
    /**
     * Removes given persistence unit and schedules update of data.
     */
    public void removePersistenceUnit(PersistenceUnit persistenceUnit){
        getPersistence().removePersistenceUnit(persistenceUnit);
        modelUpdated();
        firePropertyChange(PERSISTENCE_UNIT_ADDED_OR_REMOVED, false, true);
    }
    
    /**
     * Adds given clazz to the list of given persistence unit's managed
     * classes and schedules update of data.
     * @param persistenceUnit
     * @param clazz fully qualified name of the class to be added.
     * @param fromPanel true if added with pu design view, false if added for example with refactoring or added as new entity class in project
     * @return true if given class was added, false otherwise (for example when
     * it was already added).
     */
    public boolean addClass(PersistenceUnit persistenceUnit, String clazz, boolean fromPanel){
        String[] existing = persistenceUnit.getClass2();
        for (int i = 0; i < existing.length; i++) {
            if (clazz.equals(existing[i])){
                return false;
            }
        }
        persistenceUnit.addClass2(clazz);
        modelUpdated();
        if(!fromPanel)//may need to update panels/design view
        {
            updateUIPanels(persistenceUnit, NO_UI_PU_CLASSES_CHANGED);
        }
        return true;
    }
    
    /**
     * Removes given class from the list of given persistence unit's managed
     * classes and schedules update of data.
     * @param persistenceUnit
     * @param clazz fully qualified name of the class to be removed.
     * @param fromPanel true if added with pu design view, false if added for example with refactoring
     */
    public void removeClass(PersistenceUnit persistenceUnit, String clazz, boolean fromPanel){
        persistenceUnit.removeClass2(clazz);
        modelUpdated();
        if(!fromPanel)//may need to update panels/design view
        {
            updateUIPanels(persistenceUnit, NO_UI_PU_CLASSES_CHANGED);
        }
    }
    
    @Override
    public void showElement(Object element) {
        Object target=null;
        if (element instanceof PersistenceUnit) {
            openView(0);
            target=element;
        }
        if (target!=null) {
            final Object key=target;
            org.netbeans.modules.xml.multiview.Utils.runInAwtDispatchThread(new Runnable() {
                @Override
                public void run() {
                    getActiveMultiViewElement0().getSectionView().openPanel(key);
                }
            });
        }
    }
    
    @Override
    protected String getPrefixMark() {
        return null;
    }
    
    /** Enable to get active MultiViewElement object
     */
    public ToolBarMultiViewElement getActiveMultiViewElement0() {
        return (ToolBarMultiViewElement)super.getActiveMultiViewElement();
    }
    
    public void modelUpdated() {
        setModified(true);
        modelSynchronizer.requestUpdateData();
    }

    public boolean isChangedFromUI() {
        return changedFromUI;
    }

    public void setChangedFromUI(boolean changedFromUI) {
        this.changedFromUI=changedFromUI;
    }

    /**
     * it's used if need to update
     * @param unit
     */
    private void updateUIPanels(PersistenceUnit unit, String kind)
    {
        firePropertyChange(NO_UI_PU_CLASSES_CHANGED, null , unit);
    }

    /**
     * Call this method if the model got updated via UI, such as,visual editor
     */
    public void modelUpdatedFromUI() {
        modelSynchronizer.requestUpdateData();
    }
    
    public void updateDataFromModel(FileLock lock) throws IOException{
        modelSynchronizer.updateDataFromModel(getPersistence(), lock, true);
    }
    
    @Override
    public boolean isDeleteAllowed() {
        return true;
    }
    
    @Override
    public boolean isCopyAllowed() {
        return true;
    }
    
    @Override
    public boolean isMoveAllowed(){
        return true;
    }
    
    @Override
    protected Image getXmlViewIcon() {
        return ImageUtilities.loadImage("org/netbeans/modules/j2ee/persistence/unit/PersistenceIcon.gif"); //NOI18N
    }
    
    private class ModelSynchronizer extends XmlMultiViewDataSynchronizer {
        
        public ModelSynchronizer(XmlMultiViewDataObject dataObject) {
            super(dataObject, UPDATE_DELAY);
        }
        
        @Override
        protected boolean mayUpdateData(boolean allowDialog) {
            return true;
        }
        
        @Override
        protected void updateDataFromModel(Object model, FileLock lock, boolean modify) {
            if (model == null) {
                return;
            }
            try {
                Writer out = new StringWriter();
                ((BaseBean) model).write(out);
                out.close();
                getDataCache().setData(lock, out.toString(), modify);
            } catch (IOException e) {
                LOG.log(Level.INFO, null, e);
            } catch (Schema2BeansException e) {
                LOG.log(Level.INFO, null, e);
            } finally {
                if (lock != null){
                    lock.releaseLock();
                }
            }
        }
        
        @Override
        protected Object getModel() {
            try {
                return getPersistence();
            } catch (RuntimeException ex) { // must catch RTE (thrown by schema2beans when document is not valid)
                LOG.log(Level.INFO, null, ex);
                return null;
            }
        }
        
        @Override
        protected void reloadModelFromData() {
            parseDocument();
        }
        
    }
}
