package com.mycompany.client;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import com.google.gwt.user.client.rpc.AsyncCallback;
import com.smartgwt.client.core.DataClass;
import com.smartgwt.client.data.DSCallback;
import com.smartgwt.client.data.DSRequest;
import com.smartgwt.client.data.DSResponse;
import com.smartgwt.client.data.DataSource;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.widgets.grid.ListGridRecord;

public abstract class GwtRpcDataSource<T> extends DataSource {
   private static final String PRE_UPDATE_RECORD = "preUpdateRecord";
   private static final String DO_NOT_HANDLE = "doNotHandle";
   private static final String DO_NOT_PROPAGATE = "doNotPropagate";
   private GwtRpcDataSourceEndpoint<T> fetchEndpoint;
   private GwtRpcDataSourceEndpoint<T> updateEndpoint;
   private GwtRpcDataSourceEndpoint<T> addEndpoint;
   private GwtRpcDataSourceEndpoint<T> removeEndpoint;
   private boolean bulkMode = false;
   private Object[] parameters = new Object[0];

   public GwtRpcDataSource() {
      setClientOnly(true);
   }

   public void setFetchEndpoint(GwtRpcDataSourceEndpoint<T> fetchEndpoint) {
      this.fetchEndpoint = fetchEndpoint;
   }

   /*
    * Note if the GWT-RPC call fails the internal datasource data will be out of
    * sync with dependent widgets. This will become obvious the next time a
    * widget syncs to the datasource without triggering a fetch. One workaround
    * is to call fetchData again in postFailure, but since that kicks off an
    * extra server request that is not the default behavior.
    */
   public void setUpdateEndpoint(GwtRpcDataSourceEndpoint<T> updateEndpoint) {
      this.updateEndpoint = updateEndpoint;
   }

   public void setAddEndpoint(GwtRpcDataSourceEndpoint<T> addEndpoint) {
      this.addEndpoint = addEndpoint;
   }

   public void setRemoveEndpoint(GwtRpcDataSourceEndpoint<T> removeEndpoint) {
      this.removeEndpoint = removeEndpoint;
   }

   /*
    * If bulkMode is true the first parameter in the variable arguments for
    * edit/add/remove will be a ListGridRecord[] instead of a ListGridRecord.
    * This means the endpoint will only be called once instead of n times where
    * n is the length of the ListGridRecord array.
    */
   public void setBulkMode(boolean bulkMode) {
      this.bulkMode = bulkMode;
   }

   public void setParameters(Object... parameters) {
      this.parameters = parameters;
   }

   @Override
   public void updateData(Record record, DSCallback callback, DSRequest request) {
      request.setAttribute(PRE_UPDATE_RECORD, record);
      super.updateData(record, callback, request);
   }

   @Override
   public void updateData(Record record, DSCallback callback) {
      DSRequest request = new DSRequest();
      request.setAttribute(PRE_UPDATE_RECORD, record);
      super.updateData(record, callback);
   }

   @Override
   public void updateData(Record record) {
      DSRequest request = new DSRequest();
      request.setAttribute(PRE_UPDATE_RECORD, record);
      super.updateData(record);
   }

   @Override
   protected void transformResponse(DSResponse response, DSRequest request, Object data) {
      if (request.getOperationType() != null) {
         ListGridRecord[] records = response.getData();
         switch (request.getOperationType()) {
         case ADD:
            if (request.getAttributeAsBoolean(DO_NOT_PROPAGATE) != null && request.getAttributeAsBoolean(DO_NOT_PROPAGATE).equals(Boolean.TRUE)) {
               response.setStatus(-1);
            } else if (request.getAttributeAsBoolean(DO_NOT_HANDLE) == null || request.getAttributeAsBoolean(DO_NOT_HANDLE).equals(Boolean.FALSE)) {
               response.setStatus(-1);
               executeAdd(records);
            }
            break;
         case FETCH:
            executeFetch();
            break;
         case REMOVE:
            if (request.getAttributeAsBoolean(DO_NOT_HANDLE) == null || request.getAttributeAsBoolean(DO_NOT_HANDLE).equals(Boolean.FALSE)) {
               executeRemove(records);
               response.setStatus(-1);
            }
            break;
         case UPDATE:
            if (request.getAttributeAsBoolean(DO_NOT_HANDLE) == null || request.getAttributeAsBoolean(DO_NOT_HANDLE).equals(Boolean.FALSE)) {
               response.setStatus(-1);
               executeEdit(records, request.getAttributeAsObject(PRE_UPDATE_RECORD));
            }
            break;

         default:
            break;
         }
      }
      super.transformResponse(response, request, data);
   }

   protected abstract void populateRecord(ListGridRecord record, T object);

   private void executeEdit(final ListGridRecord[] records, final Object preUpdateRecord) {
      if (bulkMode) {
         updateEndpoint.callEndpoint(new AsyncCallback<Object>() {

            public void onFailure(Throwable caught) {
               updateEndpoint.postFailure(caught);
            }

            public void onSuccess(Object result) {
               for (int i = 0; i < records.length; i++) {
                  ListGridRecord listGridRecord = records[i];

                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_HANDLE, true);
                  updateData(listGridRecord, null, req);

                  updateEndpoint.postSuccess(result);
               }

               updateEndpoint.postSuccess(result);
            }
         }, (Object) records, parameters);
      } else {
         for (int i = 0; i < records.length; i++) {
            final ListGridRecord listGridRecord = records[i];

            updateEndpoint.callEndpoint(new AsyncCallback<Object>() {

               public void onFailure(Throwable caught) {
                  updateEndpoint.postFailure(caught);
               }

               public void onSuccess(Object result) {
                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_HANDLE, true);
                  updateData(listGridRecord, null, req);

                  updateEndpoint.postSuccess(result);
               }
            }, listGridRecord, parameters);
         }
      }
   }

   private void executeRemove(final ListGridRecord[] records) {
      if (bulkMode) {
         removeEndpoint.callEndpoint(new AsyncCallback<Object>() {

            public void onFailure(Throwable caught) {
               for (int i = 0; i < records.length; i++) {
                  ListGridRecord listGridRecord = records[i];

                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_HANDLE, true);
                  addData(listGridRecord, null, req);
               }

               removeEndpoint.postFailure(caught);
            }

            public void onSuccess(Object result) {
               for (int i = 0; i < records.length; i++) {
                  ListGridRecord listGridRecord = records[i];

                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_PROPAGATE, true);
                  addData(listGridRecord, null, req);

                  DSRequest req2 = new DSRequest();
                  req2.setAttribute(DO_NOT_HANDLE, true);
                  removeData(listGridRecord, null, req2);
               }

               removeEndpoint.postSuccess(result);
            }
         }, (Object) records, parameters);
      } else {
         for (int i = 0; i < records.length; i++) {
            final ListGridRecord listGridRecord = records[i];

            removeEndpoint.callEndpoint(new AsyncCallback<Object>() {

               public void onFailure(Throwable caught) {
                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_HANDLE, true);
                  addData(listGridRecord, null, req);

                  removeEndpoint.postFailure(caught);
               }

               public void onSuccess(Object result) {
                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_PROPAGATE, true);
                  addData(listGridRecord, null, req);

                  DSRequest req2 = new DSRequest();
                  req2.setAttribute(DO_NOT_HANDLE, true);
                  removeData(listGridRecord, null, req2);

                  removeEndpoint.postSuccess(result);
               }
            }, listGridRecord, parameters);
         }
      }
   }

   private void executeFetch() {
      fetchEndpoint.callEndpoint(new AsyncCallback<Object>() {

         public void onFailure(Throwable caught) {
            fetchEndpoint.postFailure(caught);
         }

         public void onSuccess(Object object) {
            Collection<T> resultSet = fetchEndpoint.transformResult(object);

            DataClass[] testData = getTestData();
            Map<Object, DataClass> recordSet = new HashMap<Object, DataClass>();
            for (int i = 0; i < testData.length; i++) {
               recordSet.put(testData[i].getAttribute(getPrimaryKeyFieldName()), testData[i]);
            }

            for (final T result : resultSet) {
               DSRequest req = new DSRequest();
               req.setAttribute(DO_NOT_HANDLE, true);

               final ListGridRecord record = new ListGridRecord();
               populateRecord(record, result);

               Object primaryKey = record.getAttribute(getPrimaryKeyFieldName());
               if (primaryKey != null) {
                  if (recordSet.containsKey(primaryKey)) {
                     updateData(record, null, req);
                     recordSet.remove(primaryKey);
                  } else {
                     addData(record, null, req);
                  }
               }
            }

            for (Entry<Object, DataClass> recordEntry : recordSet.entrySet()) {
               DSRequest req = new DSRequest();
               req.setAttribute(DO_NOT_HANDLE, true);

               ListGridRecord tempRecord = new ListGridRecord();
               tempRecord.setAttribute(getPrimaryKeyFieldName(), recordEntry.getValue().getAttribute(getPrimaryKeyFieldName()));
               removeData(tempRecord, null, req);
            }

            fetchEndpoint.postSuccess(object);
         }
      }, parameters);
   }

   private void executeAdd(final ListGridRecord[] records) {
      if (bulkMode) {
         removeEndpoint.callEndpoint(new AsyncCallback<Object>() {

            public void onFailure(Throwable caught) {
               for (int i = 0; i < records.length; i++) {
                  ListGridRecord listGridRecord = records[i];

                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_HANDLE, true);
                  removeData(listGridRecord, null, req);
               }

               addEndpoint.postFailure(caught);
            }

            public void onSuccess(Object result) {
               for (int i = 0; i < records.length; i++) {
                  ListGridRecord listGridRecord = records[i];

                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_HANDLE, true);
                  addData(listGridRecord, null, req);
               }

               addEndpoint.postSuccess(result);
            }
         }, (Object) records, parameters);
      } else {
         for (int i = 0; i < records.length; i++) {
            final ListGridRecord listGridRecord = records[i];

            addEndpoint.callEndpoint(new AsyncCallback<Object>() {

               public void onFailure(Throwable caught) {
                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_HANDLE, true);
                  removeData(listGridRecord, null, req);

                  addEndpoint.postFailure(caught);
               }

               public void onSuccess(Object result) {
                  DSRequest req = new DSRequest();
                  req.setAttribute(DO_NOT_HANDLE, true);
                  addData(listGridRecord, null, req);

                  addEndpoint.postSuccess(result);
               }
            }, listGridRecord, parameters);
         }
      }
   }
}
