/*
 * Copyright (C) 2011 Jan Pokorsky
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package cz.cas.lib.proarc.webapp.client.ds;

import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent;
import com.google.gwt.event.shared.HandlerRegistration;
import com.smartgwt.client.data.Criteria;
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.DataSourceField;
import com.smartgwt.client.data.Record;
import com.smartgwt.client.data.fields.DataSourceDateTimeField;
import com.smartgwt.client.data.fields.DataSourceTextField;
import com.smartgwt.client.docs.TreeDataBinding;
import com.smartgwt.client.types.CriteriaPolicy;
import com.smartgwt.client.types.DSOperationType;
import com.smartgwt.client.types.DateDisplayFormat;
import com.smartgwt.client.types.FieldType;
import com.smartgwt.client.util.BooleanCallback;
import cz.cas.lib.proarc.webapp.client.ClientUtils;
import cz.cas.lib.proarc.webapp.client.ds.DigitalObjectDataSource.DigitalObject;
import cz.cas.lib.proarc.webapp.shared.rest.DigitalObjectResourceApi;
import java.util.Arrays;
import java.util.logging.Logger;

/**
 * Data provider for member relations of digital objects.
 * <p>
 * Fetch with {@link #FIELD_ROOT} to get the tree hierarchy of members
 * including initial PID as root. See {@link TreeDataBinding} for loading tree nodes on demand.
 * TreeGrid should treat {@link #FIELD_PID} as {@code id} and {@link #FIELD_PARENT} as {@code parentId}.
 * <p>
 * Fetch with {@link #FIELD_PARENT} to get direct members.
 *
 * @author Jan Pokorsky
 */
public class RelationDataSource extends ProarcDataSource {

    private static final Logger LOG = Logger.getLogger(RelationDataSource.class.getName());
    public static final String ID = "RelationDataSource";
    private static RelationDataSource INSTANCE;

    public static final String FIELD_PID = DigitalObjectResourceApi.MEMBERS_ITEM_PID;
    public static final String FIELD_PARENT = DigitalObjectResourceApi.MEMBERS_ITEM_PARENT;
    public static final String FIELD_ROOT = DigitalObjectResourceApi.MEMBERS_ROOT_PARAM;
    public static final String FIELD_MODEL = DigitalObjectResourceApi.MEMBERS_ITEM_MODEL;
    public static final String FIELD_OWNER = DigitalObjectResourceApi.MEMBERS_ITEM_OWNER;
    public static final String FIELD_LABEL = DigitalObjectResourceApi.MEMBERS_ITEM_LABEL;
    public static final String FIELD_STATE = DigitalObjectResourceApi.MEMBERS_ITEM_STATE;
    public static final String FIELD_CREATED = DigitalObjectResourceApi.MEMBERS_ITEM_CREATED;
    public static final String FIELD_MODIFIED = DigitalObjectResourceApi.MEMBERS_ITEM_MODIFIED;
    public static final String FIELD_EXPORT = DigitalObjectResourceApi.MEMBERS_ITEM_EXPORT;
    public static final String FIELD_NDK_EXPORT = DigitalObjectResourceApi.MEMBERS_ITEM_NDK_EXPORT;
    public static final String FIELD_ARCHIVE_EXPORT = DigitalObjectResourceApi.MEMBERS_ITEM_ARCHIVE_EXPORT;
    public static final String FIELD_KRAMERIUS_EXPORT = DigitalObjectResourceApi.MEMBERS_ITEM_KRAMERIUS_EXPORT;
    public static final String FIELD_CROSSREF_EXPORT = DigitalObjectResourceApi.MEMBERS_ITEM_CROSSREF_EXPORT;
    public static final String FIELD_ORGANIZATION = DigitalObjectResourceApi.MEMBERS_ITEM_ORGANIZATION;
    public static final String FIELD_USER = DigitalObjectResourceApi.MEMBERS_ITEM_USER;
    public static final String FIELD_STATUS = DigitalObjectResourceApi.MEMBERS_ITEM_STATUS;

    /**
     * Attribute holding PIDs of the reorder update {@link #reorderChildren operation}.
     * See also {@link #transformResponse }.
     */
    private static final String ATTR_REORDER = "reorder";

    public RelationDataSource() {
        setID(ID);

        setDataURL(RestConfig.URL_DIGOBJECT_CHILDREN);

        DataSourceField pid = new DataSourceField(FIELD_PID, FieldType.TEXT);
        pid.setPrimaryKey(true);
        pid.setRequired(true);

        DataSourceField parent = new DataSourceField(FIELD_PARENT, FieldType.TEXT);
        parent.setForeignKey(ID + '.' + FIELD_PID);
        // canView:false excludes column from grid picker menu
        parent.setCanView(false);
        parent.setHidden(true);

        DataSourceField root = new DataSourceField(FIELD_ROOT, FieldType.TEXT);
        root.setHidden(true);

        DataSourceTextField model = new DataSourceTextField(FIELD_MODEL);
        DataSourceField owner = new DataSourceField(FIELD_OWNER, FieldType.TEXT);
        DataSourceField label = new DataSourceField(FIELD_LABEL, FieldType.TEXT);
        DataSourceDateTimeField created = new DataSourceDateTimeField(FIELD_CREATED);
        created.setDateFormatter(DateDisplayFormat.TOEUROPEANSHORTDATETIME);
        DataSourceDateTimeField modified = new DataSourceDateTimeField(FIELD_MODIFIED);
        modified.setDateFormatter(DateDisplayFormat.TOEUROPEANSHORTDATETIME);
        DataSourceField export = new DataSourceField(FIELD_EXPORT, FieldType.TEXT);
        DataSourceField ndkExport = new DataSourceField(FIELD_NDK_EXPORT, FieldType.TEXT);
        DataSourceField archiveExport = new DataSourceField(FIELD_ARCHIVE_EXPORT, FieldType.TEXT);
        DataSourceField krameriusExport = new DataSourceField(FIELD_KRAMERIUS_EXPORT, FieldType.TEXT);
        DataSourceField crossrefExport = new DataSourceField(FIELD_CROSSREF_EXPORT, FieldType.TEXT);
        DataSourceField organization = new DataSourceField(FIELD_ORGANIZATION, FieldType.TEXT);
        DataSourceField user = new DataSourceField(FIELD_USER, FieldType.TEXT);
        DataSourceField status = new DataSourceField(FIELD_STATUS, FieldType.TEXT);

        setFields(pid, parent, label, model, created, modified, owner, export, ndkExport, archiveExport, krameriusExport, crossrefExport, organization, user, status);
        setTitleField(FIELD_LABEL);

        setRequestProperties(RestConfig.createRestRequest(getDataFormat()));
        setOperationBindings(
                RestConfig.createAddOperation(),
                RestConfig.createDeleteOperation(),
                RestConfig.createUpdatePostOperation()
                );
        setCriteriaPolicy(CriteriaPolicy.DROPONCHANGE);
    }

    @Override
    protected Object transformRequest(DSRequest dsRequest) {
        if (dsRequest.getOperationType() == DSOperationType.UPDATE
                && RestConfig.TYPE_APPLICATION_JSON.equals(dsRequest.getContentType())) {

            return ClientUtils.dump(dsRequest.getData());
        }
        return super.transformRequest(dsRequest);
    }

    /**
     * Checks sent and returned PID lists after reorder request. If they differ
     * then invalidate cache to refresh widget records.
     */
    @Override
    protected void transformResponse(DSResponse response, DSRequest request, Object data) {
        super.transformResponse(response, request, data);
//        LOG.info("RelationDataSource.transformResponse");
        if (RestConfig.isStatusOk(response)) {
            if (request.getOperationType() == DSOperationType.UPDATE) {
                String[] oldPids = request.getAttributeAsStringArray(ATTR_REORDER);
                if (oldPids == null) {
                    return ;
                }
                String[] newPids = ClientUtils.toFieldValues(response.getData(), FIELD_PID);
                if (!Arrays.equals(oldPids, newPids)) {
                    response.setInvalidateCache(Boolean.TRUE);
                }
            }
        }
    }

    public static RelationDataSource getInstance() {
        if (INSTANCE == null) {
            INSTANCE = (RelationDataSource) DataSource.get(ID);
            // DataSource.get does not work reliably
            INSTANCE = INSTANCE != null ? INSTANCE : new RelationDataSource();
        }
        return INSTANCE;
    }

    public void addChild(String parentPid, String pid, final BooleanCallback call) {
        if (pid == null || pid.isEmpty()) {
            throw new IllegalArgumentException("Missing PID!");
        }
        addChild(parentPid, new String[] {pid}, call);
    }

    public void addChild(String parentPid, String[] pid, final BooleanCallback call) {
        if (pid == null || pid.length < 1) {
            throw new IllegalArgumentException("Missing PID!");
        }
        if (parentPid == null || parentPid.isEmpty()) {
            throw new IllegalArgumentException("Missing parent PID!");
        }

        DSRequest dsRequest = new DSRequest();

        Record update = new Record();
        update.setAttribute(RelationDataSource.FIELD_PARENT, parentPid);
        update.setAttribute(RelationDataSource.FIELD_PID, pid);
        addData(update, new DSCallback() {

            @Override
            public void execute(DSResponse response, Object rawData, DSRequest request) {
                if (!RestConfig.isStatusOk(response)) {
                    call.execute(false);
                    return;
                }
                call.execute(true);
            }
        }, dsRequest);
    }

    public void removeChild(String parentPid, String pid, final BooleanCallback call) {
        if (pid == null || pid.isEmpty()) {
            throw new IllegalArgumentException("Missing PID!");
        }
        removeChild(parentPid, new String[] {pid}, call);
    }

    public void removeChild(String parentPid, String[] pid, final BooleanCallback call) {
        if (pid == null || pid.length < 1) {
            throw new IllegalArgumentException("Missing PID!");
        }
        if (parentPid == null || parentPid.isEmpty()) {
            throw new IllegalArgumentException("Missing parent PID!");
        }

        Record update = new Record();
        update.setAttribute(RelationDataSource.FIELD_PARENT, parentPid);
        update.setAttribute(RelationDataSource.FIELD_PID, pid);
        DSRequest dsRequest = new DSRequest();
        dsRequest.setData(update); // prevents removeData to drop other than primary key attributes
        removeData(update, new DSCallback() {

            @Override
            public void execute(DSResponse response, Object rawData, DSRequest request) {
                if (!RestConfig.isStatusOk(response)) {
                    call.execute(false);
                    return;
                }
                call.execute(true);
            }
        }, dsRequest);
    }

    public void moveChild(final String pid,
            final String oldParentPid,
            final String parentPid,
            final BooleanCallback call) {

        moveChild(new String[] {pid}, oldParentPid, parentPid, call);
    }

    public void moveChild(final String[] pid,
            final String parentPidFrom,
            final String parentPidTo,
            final BooleanCallback call) {

        moveChild(pid, parentPidFrom, parentPidTo, null, call);
    }

    public void moveChild(final String[] pid,
            final String parentPidFrom,
            final String parentPidTo,
            final Integer batchId,
            final BooleanCallback call) {

        if (pid == null || pid.length < 1) {
            throw new IllegalArgumentException("Missing PID!");
        }
        if (parentPidFrom == null || parentPidFrom.isEmpty()) {
            throw new IllegalArgumentException("Missing source parent PID!");
        }
        if (parentPidTo == null || parentPidTo.isEmpty()) {
            throw new IllegalArgumentException("Missing target parent PID!");
        }
        Record update = new Record();
        update.setAttribute(DigitalObjectResourceApi.MEMBERS_MOVE_SRCPID, parentPidFrom);
        update.setAttribute(DigitalObjectResourceApi.MEMBERS_MOVE_DSTPID, parentPidTo);
        update.setAttribute(RelationDataSource.FIELD_PID, pid);
        if (batchId != null) {
            update.setAttribute(DigitalObjectResourceApi.MEMBERS_ITEM_BATCHID, batchId);
        }
        final RelationDataSource ds = RelationDataSource.getInstance();
        DSRequest request = new DSRequest();
        request.setActionURL(RestConfig.URL_DIGOBJECT_CHILDREN_MOVE);
        ds.updateData(update, new DSCallback() {

            @Override
            public void execute(DSResponse response, Object data, DSRequest request) {
                if (!RestConfig.isStatusOk(response)) {
                    call.execute(false);
                    return;
                }
                call.execute(true);
            }
        }, request);
    }

    /**
     * Saves new children sequence for a given parent object.
     * 
     * @param parent parent object
     * @param childPids children sequence
     * @param call callback
     */
    public void reorderChildren(DigitalObject parent, String[] childPids, final BooleanCallback call) {
        if (childPids == null || childPids.length < 2) {
            throw new IllegalArgumentException("Unexpected children: " + Arrays.toString(childPids));
        }
        if (parent == null) {
            throw new NullPointerException("parent");
        }

        DSRequest dsRequest = new DSRequest();
        dsRequest.setAttribute(ATTR_REORDER, childPids);

        Record update = new Record();
        update.setAttribute(RelationDataSource.FIELD_PID, childPids);
        String parentPid = parent.getPid();
        String batchId = parent.getBatchId();
        if (batchId != null) {
            update.setAttribute(DigitalObjectResourceApi.MEMBERS_ITEM_BATCHID, batchId);
        } else {
            update.setAttribute(RelationDataSource.FIELD_PARENT, parentPid);
        }
        updateData(update, new DSCallback() {

            @Override
            public void execute(DSResponse response, Object rawData, DSRequest request) {
                if (!RestConfig.isStatusOk(response)) {
                    call.execute(false);
                    return;
                }
                call.execute(true);
            }
        }, dsRequest);
    }

    /**
     * Compares arrays of PIDS as array of records.
     */
    public static boolean equals(Record[] rs1, Record[] rs2) {
        if (rs1 == null && rs1 == rs2) {
            return true;
        }
        if (rs1 == null || rs2 == null) {
            return false;
        }
        if (rs1.length != rs2.length) {
            return false;
        }
        for (int i = 0; i < rs1.length; i++) {
            String pid1 = rs1[i].getAttribute(RelationDataSource.FIELD_PID);
            String pid2 = rs2[i].getAttribute(RelationDataSource.FIELD_PID);
            if (!pid1.equals(pid2)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Invoked by other digital object editors that can change object label.
     * @param pid PID of modified object
     */
    public void fireRelationChange(String pid) {
        fireEvent(new RelationChangeEvent(pid));
    }

    /**
     * Fetches relations of parent object and update caches to notify widgets.
     * @param parentPid PID of parent object
     * @param callback the callback to run on finish
     */
    public final void updateCaches(String parentPid, final BooleanCallback callback) {
        Criteria criteria = new Criteria(RelationDataSource.FIELD_ROOT, parentPid);
        criteria.addCriteria(RelationDataSource.FIELD_PARENT, parentPid);
        fetchData(criteria, new DSCallback() {

            @Override
            public void execute(DSResponse response, Object rawData, DSRequest request) {
                request.setOperationType(DSOperationType.UPDATE);
                updateCaches(response, request);
                callback.execute(Boolean.TRUE);
            }
        });
    }

    public final HandlerRegistration addRelationChangeHandler(RelationChangeHandler handler) {
        return doAddHandler(handler, RelationChangeEvent.TYPE);
    }

    /**
     * Notifies changes of relation labels.
     */
    public static interface RelationChangeHandler extends EventHandler {

        void onRelationChange(RelationChangeEvent event);

    }

    public static final class RelationChangeEvent extends GwtEvent<RelationChangeHandler> {

        public static final Type<RelationChangeHandler> TYPE = new Type<RelationChangeHandler>();
        private final String pid;

        public RelationChangeEvent() {
            this(null);
        }

        public RelationChangeEvent(String pid) {
            this.pid = pid;
        }

        public String getPid() {
            return pid;
        }

        @Override
        public Type<RelationChangeHandler> getAssociatedType() {
            return TYPE;
        }

        @Override
        protected void dispatch(RelationChangeHandler handler) {
            handler.onRelationChange(this);
        }
    }

}