/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2004-2006, Geotools Project Managment Committee (PMC)
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.data.wfs;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.naming.OperationNotSupportedException;

import org.geotools.data.Transaction;
import org.geotools.data.Transaction.State;
import org.geotools.data.wfs.Action.DeleteAction;
import org.geotools.data.wfs.Action.InsertAction;
import org.geotools.data.wfs.Action.UpdateAction;
import org.geotools.filter.FidFilter;
import org.geotools.util.logging.Logging;
import org.opengis.filter.Filter;
import org.geotools.xml.DocumentFactory;
import org.geotools.xml.DocumentWriter;
import org.geotools.xml.wfs.WFSSchema;
import org.xml.sax.SAXException;

/**
 * DOCUMENT ME!
 * 
 * @author dzwiers TODO To change the template for this generated type comment go to Window -
 *         Preferences - Java - Code Style - Code Templates
 * @source $URL:
 *         http://svn.geotools.org/geotools/branches/2.2.x/plugin/wfs/src/org/geotools/data/wfs/WFSTransactionState.java $
 */
public class WFSTransactionState implements State {
    private WFSDataStore ds        = null;
    /**
     * A map of <String, String[]>. String is the typename and String[] are the fids returned by
     * Transaction Results during the last commit. They are the fids of the inserted elements.
     */
    private Map          fids      = new HashMap();
    /**
     * A Map of <String, List<Action>> where string is the typeName of the feature type and the
     * list is the list of actions that have modified the feature type
     */
    Map          actionMap = new HashMap();
    
    private long latestFid=Long.MAX_VALUE;

    private WFSTransactionState() {
        // should not be used
    }

    /**
     * @param ds
     */
    public WFSTransactionState( WFSDataStore ds ) {
        this.ds = ds; 
    }

    /**
     * @see org.geotools.data.Transaction.State#setTransaction(org.geotools.data.Transaction)
     */
    public void setTransaction( Transaction transaction ) {
        if (transaction != null) {
            synchronized (actionMap) {
                synchronized (fids) {
                    fids.clear();
                }
                actionMap.clear();
            }
        }
    }

    /**
     * @see org.geotools.data.Transaction.State#addAuthorization(java.lang.String)
     */
    public void addAuthorization( String AuthID ) {
        // authId = AuthID;
    }

    /**
     * Not implemented
     * 
     * @return String
     */
    public String getLockId() {
        return null; // add this later
    }

    /**
     * @see org.geotools.data.Transaction.State#commit()
     */
    public void commit() throws IOException {
        // TODO deal with authID and locking ... WFS only allows one authID / transaction ...
        TransactionResult tr = null;

        Map copiedActions;
        synchronized (actionMap) {
            combineActions();
            copiedActions = copy(actionMap);
        }
        Iterator iter = copiedActions.entrySet().iterator();
        while( iter.hasNext() ) {
            Map.Entry entry = (Entry) iter.next();
            List actions = (List) entry.getValue();
            String typeName = (String) entry.getKey();

            if (actions.isEmpty())
                continue;

            if (((ds.protocol & WFSDataStore.POST_PROTOCOL) == WFSDataStore.POST_PROTOCOL)
                    && (tr == null)) {
                try {
                    tr = commitPost(actions);
                } catch (OperationNotSupportedException e) {
                    WFSDataStoreFactory.logger.warning(e.toString());
                    tr = null;
                } catch (SAXException e) {
                    WFSDataStoreFactory.logger.warning(e.toString());
                    tr = null;
                }
            }

            // if (((ds.protocol & WFSDataStore.GET_PROTOCOL) == WFSDataStore.GET_PROTOCOL)
            // && (tr == null)) {
            // try {
            // tr = commitPost();
            // } catch (OperationNotSupportedException e) {
            // WFSDataStoreFactory.logger.warning(e.toString());
            // tr = null;
            // } catch (SAXException e) {
            // WFSDataStoreFactory.logger.warning(e.toString());
            // tr = null;
            // }
            // }

            if (tr == null) {
                throw new IOException("An error occured while committing.");
            }

            if (tr.getStatus() == TransactionResult.FAILED) {
                throw new IOException(tr.getError().toString());
            }

            List newFids = tr.getInsertResult();
            int currentInsertIndex = 0;
            for( Iterator iter2 = actions.iterator(); iter2.hasNext(); ) {
                Object action = iter2.next();
                if (action instanceof InsertAction) {
                    InsertAction insertAction = (InsertAction) action;
                    if (currentInsertIndex >= newFids.size()) {
                        Logging.getLogger("org.geotools.data.wfs").severe(
                                "Expected more fids to be returned by " + "TransactionResponse!");
                        break;
                    }
                    ds.addFidMapping(insertAction.getFeature().getID(), (String) newFids
                            .get(currentInsertIndex));
                    currentInsertIndex++;
                }
            }
            synchronized (this.fids) {
                this.fids.put(typeName, (String[]) newFids.toArray(new String[0]));
            }

            if (currentInsertIndex != newFids.size()) {
                Logging.getLogger("org.geotools.data.wfs").severe(
                        "number of fids inserted do not match number of fids returned "
                                + "by Transaction Response.  Got:" + newFids.size() + " expected: "
                                + currentInsertIndex);
            }
            synchronized (actionMap) {
                ((List) actionMap.get(typeName)).removeAll(actions);
            }
        }
    }

    private Map copy( Map actionMap2 ) {
        Map newMap=new HashMap();
        Iterator entries=actionMap2.entrySet().iterator();
        while( entries.hasNext() ){
            Map.Entry entry=(Entry) entries.next();
            List list=(List) entry.getValue();
            newMap.put(entry.getKey(), new ArrayList(list));
        }
        return newMap;
    }

    private TransactionResult commitPost( List toCommit ) throws OperationNotSupportedException,
            IOException, SAXException {
        URL postUrl = ds.capabilities.getTransaction().getPost();

        // System.out.println("POST Commit URL = "+postUrl);

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

        HttpURLConnection hc = ds.getConnection(postUrl, ds.auth, true);
        // System.out.println("connection to commit");
        Map hints = new HashMap();
        hints.put(DocumentWriter.BASE_ELEMENT, WFSSchema.getInstance().getElements()[24]); // Transaction
        Set fts = new HashSet();
        Iterator i = toCommit.iterator();
        while( i.hasNext() ) {
            Action a = (Action) i.next();
            fts.add(a.getTypeName());
        }
        Set ns = new HashSet();
        ns.add(WFSSchema.NAMESPACE.toString());
        i = fts.iterator();
        while( i.hasNext() ) {
            ns.add(ds.getSchema((String) i.next()).getNamespace().toString());
        }
        hints.put(DocumentWriter.SCHEMA_ORDER, ns.toArray(new String[ns.size()])); // Transaction

        // System.out.println("Ready to print Debug");
        // // DEBUG
        // StringWriter debugw = new StringWriter();
        // DocumentWriter.writeDocument(this, WFSSchema.getInstance(), debugw, hints);
        // System.out.println("TRANSACTION \n\n");
        // System.out.println(debugw.getBuffer());
        // // END DEBUG

        OutputStream os = hc.getOutputStream();

        // write request
        Writer w = new OutputStreamWriter(os);
        Logger logger = Logging.getLogger("org.geotools.data.wfs");
        if (logger.isLoggable(Level.FINE)) {
            w = new LogWriterDecorator(w, logger, Level.FINE);
        }
        // special logger for communication information only.
        logger = Logging.getLogger("org.geotools.data.communication");
        if (logger.isLoggable(Level.FINE) ){
            w = new LogWriterDecorator(w, logger, Level.FINE);
        }

        DocumentWriter.writeDocument(this, WFSSchema.getInstance(), w, hints);
        w.flush();
        w.close();

        InputStream is = this.ds.getInputStream(hc);

        hints = new HashMap();

        TransactionResult ft = (TransactionResult) DocumentFactory.getInstance(is, hints,
                Level.WARNING);
        return ft;
    }

    /**
     * @see org.geotools.data.Transaction.State#rollback()
     */
    public void rollback() {
        synchronized (actionMap) {
            actionMap.clear();
        }
    }

    /**
     * @return Fid Set
     */
    public String[] getFids( String typeName ) {
        synchronized (fids) {
            return (String[]) fids.get(typeName);
        }
    }

    /**
     * @param a
     */
    public void addAction( String typeName, Action a ) {
        synchronized (actionMap) {
            List list = (List) actionMap.get(typeName);
            if( list==null ){
            	list=new ArrayList();
            	actionMap.put(typeName, list);
            }
			list.add(a);
        }
    }

    /**
     * @return List of Actions
     */
    public List getActions( String typeName ) {
        synchronized (actionMap) {
            Collection collection = (Collection) actionMap.get(typeName);
            if( collection==null || collection.isEmpty())
            	return new ArrayList();
			return new ArrayList(collection);
        }
    }
    
    /**
     * Returns all the actions for all FeatureTypes
     *
     * @return all the actions for all FeatureTypes
     */
    public List getAllActions() {
        synchronized (actionMap) {
            List all=new ArrayList();
            for( Iterator iter = actionMap.values().iterator(); iter.hasNext(); ) {
                List actions = (List) iter.next();
            	all.addAll(actions);
            }
            return all;
        }
    }

    /**
     * Combines updates and inserts reducing the number of actions in the commit.
     * <p>
     * This is in response to an issue where the FID is not known until after the commit so if a
     * Feature is inserted then later updated(using a FID filter to identify the feature to update)
     * within a single transactin then the commit will fail because the fid filter will be not apply
     * once the insert action is processed.
     * </p>
     * <p>
     * For Example:
     * <ol>
     * <li>Insert Feature.
     * <p>
     * Transaction assigns it the id: NewFeature.
     * </p>
     * </li>
     * <li>Update Feature.
     * <p>
     * Fid filter is used to update NewFeature.
     * </p>
     * </li>
     * <li>Commit.
     * <p>
     * Update will fail because when the Insert action is processed NewFeature will not refer to any
     * feature.
     * </p>
     * </li>
     * </ol>
     * </p>
     * <p>
     * The algorithm is essentially foreach( insertAction ){ Apply each update and Delete action
     * that applies to the inserted feature move insertAction to end of list }
     * </p>
     * <p>
     * Mind you this only works assuming there aren't any direct dependencies between the actions
     * beyond the ones specified by the API. For example if the value of an update depends directly
     * on an earlier feature object (which is bad practice and should never be done). Then we may
     * have problems with this solution. But I think that this solution is better than doing nothing
     * because at least in the proper use of the API the correct result will be obtained. Whereas
     * before the correct use of the API could obtain incorrect results.
     * </p>
     */
    protected void combineActions() {
        EACH_FEATURE_TYPE: for( Iterator iter = actionMap.values().iterator(); iter.hasNext(); ) {
            List actions = (List) iter.next();
            
        removeFilterAllActions(actions);
        InsertAction firstAction = null;
        while( firstAction == null || !actions.contains(firstAction) ) {
            firstAction = findFirstInsertAction(actions);
            if (firstAction == null)
                continue EACH_FEATURE_TYPE;
            processInsertAction(actions, firstAction);
        }
        InsertAction current = findFirstInsertAction(actions);
        while( current != null && firstAction != current ) {
            processInsertAction(actions, current);
            current = findFirstInsertAction(actions);
        }
        }
    }

    /**
     * Removes all actions whose filter is Filter.EXCLUDE
     */
    private void removeFilterAllActions(List actions) {
        for( Iterator iter = actions.iterator(); iter.hasNext(); ) {
            Action element = (Action) iter.next();
            if (Filter.EXCLUDE.equals(element.getFilter()))
                iter.remove();
        }
    }

    private InsertAction findFirstInsertAction(List actions) {
        int i = 0;
        for( Iterator iter = actions.iterator(); iter.hasNext(); i++ ) {
            Object action = iter.next();
            if (action instanceof InsertAction) {
                return (InsertAction) action;
            }
        }
        return null;
    }

    private void processInsertAction( List actions, InsertAction action ) {
        int indexOf = actions.indexOf(action);
        while( indexOf + 1 < actions.size() && indexOf != -1 ) {
            moveUpdateAndMoveInsertAction(actions, indexOf, action);
            indexOf = actions.indexOf(action);
        }
    }

    private void moveUpdateAndMoveInsertAction( List actions, int i, InsertAction action ) {
        if (i + 1 < actions.size()) {
            Object nextAction = actions.get(i + 1);
            if (nextAction instanceof DeleteAction) {
                handleDeleteAction(actions, i, action, (DeleteAction) nextAction);
            } else if (nextAction instanceof UpdateAction) {
                handleUpdateAction(actions, i, action, (UpdateAction) nextAction);
            } else
                swap(actions, i);
        }
    }

    private void handleDeleteAction( List actions, int i, InsertAction action, DeleteAction deleteAction ) {
        // if inserted action has been deleted then remove action
        if (deleteAction.getFilter().evaluate(action.getFeature())) {
            actions.remove(i);
            // if filter is a fid filter of size 1 then it only contains the inserted feature which
            // no longer exists since it has been deleted. so remove that action as well.
            if (deleteAction.getFilter() instanceof FidFilter
                    && ((FidFilter) deleteAction.getFilter()).getFids().length == 1) {
                actions.remove(i);
            }
        } else {
            swap(actions, i);
        }
    }

    private int handleUpdateAction( List actions, int i, InsertAction action, UpdateAction updateAction ) {
        // if update action applies to feature then update feature
        if (updateAction.getFilter().evaluate(action.getFeature())) {
            updateAction.update(action.getFeature());
            // if filter is a fid filter and there is only 1 fid then filter uniquely identifies
            // only the
            // one features so remove it.
            if (updateAction.getFilter() instanceof FidFilter
                    && ((FidFilter) updateAction.getFilter()).getFids().length == 1) {
                actions.remove(i + 1);
                return i;
            }
        }
        swap(actions, i);
        return i + 1;
    }

    /**
     * swaps the action at location i with the item at location i+1
     * 
     * @param i item to swap
     */
    private void swap( List actions, int i ) {
        Object item = actions.remove(i);
        actions.add(i + 1, item);
    }

    public String nextFid( String typeName ) {
        long fid;
        synchronized (this) {
            fid = latestFid;
            latestFid--;
        }
        return "new"+typeName+"."+fid;
    }

}
