/*
 * Copyright (C) 2013 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.action;

import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceController;
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.Record;
import com.smartgwt.client.data.RecordList;
import com.smartgwt.client.data.ResultSet;
import com.smartgwt.client.types.PromptStyle;
import com.smartgwt.client.util.Page;
import com.smartgwt.client.util.SC;
import cz.cas.lib.proarc.common.object.model.DatastreamEditorType;
import cz.cas.lib.proarc.webapp.client.ClientMessages;
import cz.cas.lib.proarc.webapp.client.ds.DigitalObjectDataSource.DigitalObject;
import cz.cas.lib.proarc.webapp.client.ds.RelationDataSource;
import cz.cas.lib.proarc.webapp.client.ds.RestConfig;
import cz.cas.lib.proarc.webapp.client.ds.SearchDataSource;
import cz.cas.lib.proarc.webapp.client.presenter.DigitalObjectEditing.DigitalObjectEditorPlace;

/**
 * Opens parent, child or sibling of selected digital object in the current editor.
 * In order to navigate to given child object the action source has to implement
 * {@link ChildSelector}. Otherwise the first child is used.
 *
 * @author Jan Pokorsky
 */
public final class DigitalObjectNavigateAction extends AbstractAction {

    public enum Navigation {CHILD, NEXT, PARENT, PREV}

    private final PlaceController places;
    private final ClientMessages i18n;
    private final Navigation navigation;
    private static RecordList siblings;

    public static DigitalObjectNavigateAction parent(ClientMessages i18n, PlaceController places) {
        return new DigitalObjectNavigateAction(i18n,
                i18n.DigitalObjectNavigateAction_OpenParent_Title(),
                Page.getAppDir() + "images/16/next_up.png",
                i18n.DigitalObjectNavigateAction_OpenParent_Hint(),
                Navigation.PARENT,
                places);
    }

    public static DigitalObjectNavigateAction child(ClientMessages i18n, PlaceController places) {
        return new DigitalObjectNavigateAction(i18n,
                i18n.DigitalObjectNavigateAction_OpenChild_Title(),
                Page.getAppDir() + "images/16/next_down.png",
                i18n.DigitalObjectNavigateAction_OpenChild_Hint(),
                Navigation.CHILD,
                places);
    }

    public static DigitalObjectNavigateAction next(ClientMessages i18n, PlaceController places) {
        return new DigitalObjectNavigateAction(i18n,
                i18n.DigitalObjectNavigateAction_OpenNext_Title(),
                "[SKIN]/actions/next.png",
                i18n.DigitalObjectNavigateAction_OpenNext_Hint(),
                Navigation.NEXT,
                places);
    }

    public static DigitalObjectNavigateAction previous(ClientMessages i18n, PlaceController places) {
        return new DigitalObjectNavigateAction(i18n,
                i18n.DigitalObjectNavigateAction_OpenPrevious_Title(),
                "[SKIN]/actions/prev.png",
                i18n.DigitalObjectNavigateAction_OpenPrevious_Hint(),
                Navigation.PREV,
                places);
    }

    public DigitalObjectNavigateAction(
            ClientMessages i18n, String title, String icon, String tooltip, Navigation navigation,
            PlaceController places) {

        super(title, icon, tooltip);
        this.navigation = navigation;
        this.places = places;
        this.i18n = i18n;
    }

    @Override
    public void performAction(ActionEvent event) {
        Record[] selectedRecords = Actions.getSelection(event);
        if (accept(selectedRecords)) {
            DigitalObject dobj = DigitalObject.create(selectedRecords[0]);
            String pid = dobj.getPid();
            switch (navigation) {
                case PARENT:
                    openParent(pid);
                    break;
                case NEXT:
                case PREV:
                    openSibling(pid, false);
                    break;
                case CHILD:
                    openChild(pid, getChildSelection(event));
                    break;
            }
        }
    }

    @Override
    public boolean accept(ActionEvent event) {
        Record[] selectedRecords = Actions.getSelection(event);
        return accept(selectedRecords);
    }

    private boolean accept(Record[] selectedRecords) {
        if (selectedRecords != null && selectedRecords.length == 1) {
            return isDigitalObject(selectedRecords[0]);
        }
        return false;
    }

    private static boolean isDigitalObject(Record r) {
        DigitalObject dobj = DigitalObject.createOrNull(r);
        return dobj != null;
    }

    private void openParent(final String childPid) {
        if (childPid != null) {
            SearchDataSource.getInstance().findParent(childPid, null, new Callback<ResultSet, Void>() {

                @Override
                public void onFailure(Void reason) {
                }

                @Override
                public void onSuccess(ResultSet result) {
                    if (result.isEmpty()) {
                        SC.warn(i18n.DigitalObjectNavigateAction_NoParent_Msg());
                    } else {
                        Record parent = result.first();
                        DigitalObject parentObj = DigitalObject.createOrNull(parent);
                        if (parentObj != null) {
                            siblings = null;
                            DigitalObjectEditorPlace place = new DigitalObjectEditorPlace(
                                    getLastEditorId(), parentObj);
                            place.setSelectChildPid(childPid);
                            places.goTo(place);
                        }
                    }
                }
            });
        }
    }

    private void open(DigitalObject dobj) {
        if (dobj != null) {
            DigitalObjectEditorPlace place = new DigitalObjectEditorPlace(
                    getLastEditorId(), dobj);
            places.goTo(place);
        }
    }

    /**
     * Opens a child object in the editor. If child is {@code null} it fetches
     * children and opens the first one.
     */
    private void openChild(final String parentPid, DigitalObject child) {
        if (child != null) {
            siblings = null;
            open(child);
            return ;
        }
        Criteria criteria = new Criteria(RelationDataSource.FIELD_ROOT, parentPid);
        criteria.addCriteria(RelationDataSource.FIELD_PARENT, parentPid);
        RelationDataSource.getInstance().fetchData(criteria, new DSCallback() {

            @Override
            public void execute(DSResponse dsResponse, Object data, DSRequest dsRequest) {
                if (RestConfig.isStatusOk(dsResponse)) {
                    RecordList result = dsResponse.getDataAsRecordList();
                    DigitalObject dobj = null;
                    if (!result.isEmpty()) {
                        dobj = DigitalObject.createOrNull(result.get(0));
                    }
                    if (dobj != null) {
                        siblings = result;
                        open(dobj);
                    } else {
                        // No child
                        SC.warn(i18n.DigitalObjectNavigateAction_NoChild_Msg());
                    }
                }
            }
        }, createRequestWithPrompt());
    }

    private void openSibling(final String pid, boolean cached) {
        if (pid != null) {
            RecordList rs = getSiblings();
            int pidIndex = rs.findIndex(RelationDataSource.FIELD_PID, pid);
            if (pidIndex == -1) {
                // fetch
                if (cached) {
                    // not found PID
                    SC.warn("Not found " + pid);
                } else {
                    fetchSiblings(pid);
                }
            } else if (navigation == Navigation.PREV && pidIndex == 0) {
                SC.warn(i18n.DigitalObjectNavigateAction_NoPrevSibling_Msg());
            } else if (navigation == Navigation.NEXT && pidIndex + 1 >= rs.getLength()) {
                SC.warn(i18n.DigitalObjectNavigateAction_NoNextSibling_Msg());
            } else {
                // open new
                int inc = navigation == Navigation.PREV ? -1 : 1;
                DigitalObject newObj = DigitalObject.create(rs.get(pidIndex + inc));
                if (newObj != null) {
                    open(newObj);
                }
            }
        }
    }

    private RecordList getSiblings() {
        if (siblings == null) {
            // listen to RelationDataSource updates to invalidate cache?
            siblings = new RecordList();
        }
        return siblings;
    }

    private void fetchSiblings(final String pid) {
        SearchDataSource.getInstance().findParent(pid, null, new Callback<ResultSet, Void>() {

            @Override
            public void onFailure(Void reason) {
            }

            @Override
            public void onSuccess(ResultSet result) {
                if (result.isEmpty()) {
                    SC.warn(i18n.DigitalObjectNavigateAction_NoParent_Msg());
                } else {
                    Record parent = result.first();
                    DigitalObject parentObj = DigitalObject.createOrNull(parent);
                    if (parentObj != null) {
                        scheduleFetchSiblings(parentObj.getPid(), pid);
                    }
                }
            }
        });
    }

    /**
     * Postpones {@link #fetchSiblings(java.lang.String, java.lang.String) fetch}
     * to force RPCManager to notify user with the request prompt. The invocation
     * from ResultSet's DataArrivedHandler ignores prompt settings and ResultSet
     * does not provide possibility to declare the prompt.
     */
    private void scheduleFetchSiblings(final String parentPid, final String pid) {
        Scheduler.get().scheduleDeferred(new ScheduledCommand() {

            @Override
            public void execute() {
                fetchSiblings(parentPid, pid);
            }
        });
    }

    private void fetchSiblings(final String parentPid, final String pid) {
        Criteria criteria = new Criteria(RelationDataSource.FIELD_ROOT, parentPid);
        criteria.addCriteria(RelationDataSource.FIELD_PARENT, parentPid);
        RelationDataSource.getInstance().fetchData(criteria, new DSCallback() {

            @Override
            public void execute(DSResponse dsResponse, Object data, DSRequest dsRequest) {
                if (RestConfig.isStatusOk(dsResponse)) {
                    siblings = dsResponse.getDataAsRecordList();
                    openSibling(pid, true);
                }
            }
        }, createRequestWithPrompt());
    }

    private DatastreamEditorType getLastEditorId() {
        DatastreamEditorType editorId = null;
        Place where = places.getWhere();
        if (where instanceof DigitalObjectEditorPlace) {
            DigitalObjectEditorPlace editorPlace = (DigitalObjectEditorPlace) where;
            editorId = editorPlace.getEditorId();
        }
        return editorId == null ? DatastreamEditorType.CHILDREN : editorId;
    }

    private static DigitalObject getChildSelection(ActionEvent event) {
        Object source = event.getSource();
        if (source instanceof ChildSelector) {
            ChildSelector selectable = (ChildSelector) source;
            Record[] children = selectable.getChildSelection();
            if (children != null && children.length > 0) {
                return DigitalObject.createOrNull(children[0]);
            }
        }
        return null;
    }

    private static DSRequest createRequestWithPrompt() {
        DSRequest dsRequest = new DSRequest();
        dsRequest.setPromptStyle(PromptStyle.CURSOR);
        dsRequest.setShowPrompt(true);
        return dsRequest;
    }

    /**
     * The action source should implement this to supply children in case of
     * the child navigation.
     */
    public interface ChildSelector {

        /**
         * Gets an array of child records.
         */
        Record[] getChildSelection();

    }
}