/*****************************************************************
 *   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.apache.cayenne.modeler.dialog.objentity;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.apache.log4j.Logger;
import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbRelationship;
import org.apache.cayenne.map.Entity;
import org.apache.cayenne.map.ObjEntity;
import org.apache.cayenne.map.ObjRelationship;
import org.apache.cayenne.map.Relationship;
import org.apache.cayenne.modeler.util.Comparators;
import org.apache.cayenne.util.Util;
import org.scopemvc.core.IntIndexSelector;
import org.scopemvc.core.ModelChangeEvent;
import org.scopemvc.core.ModelChangeTypes;
import org.scopemvc.core.Selector;
import org.scopemvc.model.basic.BasicModel;
import org.scopemvc.model.collection.ListModel;

/**
 * A Scope model for mapping an ObjRelationship to one or 
 * more DbRelationships.
 * 
 * @since 1.1
 * @author Andrei Adamchik
 */
public class ObjRelationshipInfoModel extends BasicModel {
    static final Logger logObj = Logger.getLogger(ObjRelationshipInfoModel.class);


    public static final Selector DB_RELATIONSHIP_PATH_SELECTOR =
        Selector.fromString("dbRelationshipPath");
    public static final Selector SOURCE_ENTITY_NAME_SELECTOR =
        Selector.fromString("relationship.sourceEntity.name");
    public static final Selector SELECTED_PATH_COMPONENT_SELECTOR =
        Selector.fromString("selectedPathComponent");
    public static final Selector OBJECT_TARGET_SELECTOR =
        Selector.fromString("objectTarget");
    public static final Selector OBJECT_TARGETS_SELECTOR =
        Selector.fromString("objectTargets");
    public static final Selector RELATIONSHIP_NAME_SELECTOR =
        Selector.fromString("relationshipName");

    protected ObjRelationship relationship;
    protected ListModel dbRelationshipPath;
    protected EntityRelationshipsModel selectedPathComponent;
    protected ObjEntity objectTarget;
    protected List objectTargets;
    protected String relationshipName;

    public ObjRelationshipInfoModel(
        ObjRelationship relationship,
        Collection objEntities) {

        this.relationship = relationship;
        this.relationshipName = relationship.getName();
        this.objectTarget = (ObjEntity) relationship.getTargetEntity();

        // prepare entities - copy those that have DbEntities mapped, and then sort

        this.objectTargets = new ArrayList(objEntities.size());
        Iterator entities = objEntities.iterator();
        while (entities.hasNext()) {
            ObjEntity entity = (ObjEntity) entities.next();
            if (entity.getDbEntity() != null) {
                objectTargets.add(entity);
            }
        }

        Collections.sort(objectTargets, Comparators.getNamedObjectComparator());

        // validate -
        // current limitation is that an ObjRelationship must have source 
        // and target entities present, with DbEntities chosen.
        validateCanMap();

        // wrap path
        this.dbRelationshipPath = new ListModel();
        Iterator it = relationship.getDbRelationships().iterator();
        while (it.hasNext()) {
            DbRelationship dbRelationship = (DbRelationship) it.next();
            this.dbRelationshipPath.add(new EntityRelationshipsModel(dbRelationship));
        }

        // add dummy last relationship if we are not connected
        connectEnds();
        this.dbRelationshipPath.addModelChangeListener(this);
    }

    public ObjRelationship getRelationship() {
        return relationship;
    }

    public ListModel getDbRelationshipPath() {
        return dbRelationshipPath;
    }

    public EntityRelationshipsModel getSelectedPathComponent() {
        return selectedPathComponent;
    }

    public void setSelectedPathComponent(EntityRelationshipsModel selectedPathComponent) {
        if (this.selectedPathComponent != selectedPathComponent) {
            unlistenOldSubmodel(SELECTED_PATH_COMPONENT_SELECTOR);
            this.selectedPathComponent = selectedPathComponent;
            listenNewSubmodel(SELECTED_PATH_COMPONENT_SELECTOR);
            fireModelChange(
                ModelChangeTypes.VALUE_CHANGED,
                SELECTED_PATH_COMPONENT_SELECTOR);
        }
    }

    /**
     * Returns currently selected target of the ObjRelationship.
     */
    public ObjEntity getObjectTarget() {
        return objectTarget;
    }

    /** 
     * Sets a new target 
     */
    public void setObjectTarget(ObjEntity objectTarget) {
        if (this.objectTarget != objectTarget) {
            unlistenOldSubmodel(OBJECT_TARGET_SELECTOR);
            this.objectTarget = objectTarget;
            listenNewSubmodel(OBJECT_TARGET_SELECTOR);
            fireModelChange(ModelChangeTypes.VALUE_CHANGED, OBJECT_TARGET_SELECTOR);

            // change the list of relationships 
            breakChain(-1);
            connectEnds();
            fireModelChange(
                ModelChangeTypes.VALUE_CHANGED,
                DB_RELATIONSHIP_PATH_SELECTOR);
        }
    }

    /**
     * Returns a list of ObjEntities available for target mapping.
     */
    public List getObjectTargets() {
        return objectTargets;
    }

    public String getRelationshipName() {
        return relationshipName;
    }

    public void setRelationshipName(String relationshipName) {
        this.relationshipName = relationshipName;
    }

    public void modelChanged(ModelChangeEvent event) {

        // if a different relationship was selected, we may need to rebuild the list
        Selector selector = event.getSelector();
        while (selector != null) {
            if (selector instanceof IntIndexSelector) {
                IntIndexSelector indexSel = (IntIndexSelector) selector;
                relationshipChanged(indexSel.getIndex());
                break;
            }

            selector = selector.getNext();
        }

        super.modelChanged(event);
    }

    /**
     * Processes relationship path when path component at index was changed.
     */
    public synchronized void relationshipChanged(int index) {
        // strip everything starting from the index
        breakChain(index);

        // connect the ends
        connectEnds();
        
        // must fire with null selector, or refresh won't happen
        dbRelationshipPath.fireModelChange(VALUE_CHANGED, null);
    }

    /**
     * Stores current state of the model in the internal ObjRelationship.
     */
    public synchronized boolean savePath() {
        // check for modifications
        if (relationship.getTargetEntity() == objectTarget) {
            if (Util.nullSafeEquals(relationship.getName(), relationshipName)) {
                List oldPath = relationship.getDbRelationships();
                if (oldPath.size() == dbRelationshipPath.size()) {
                    boolean hasChanges = false;
                    for (int i = 0; i < oldPath.size(); i++) {
                        EntityRelationshipsModel next =
                            (EntityRelationshipsModel) dbRelationshipPath.get(i);
                        if (oldPath.get(i) != next.getSelectedRelationship()) {
                            hasChanges = true;
                            break;
                        }
                    }

                    if (!hasChanges) {
                        return false;
                    }
                }
            }
        }

        // detected modifications, save...
        relationship.clearDbRelationships();

        // note on events notification - this needs to be propagated
        // via old modeler events, but we leave this to the controller
        // since model knows nothing about Modeler mediator.
        relationship.setTargetEntity(objectTarget);
        relationship.setName(relationshipName);

        Iterator it = dbRelationshipPath.iterator();
        while (it.hasNext()) {
            EntityRelationshipsModel next = (EntityRelationshipsModel) it.next();
            Relationship nextPathComponent = next.getSelectedRelationship();
            if (nextPathComponent == null) {
                break;
            }

            relationship.addDbRelationship((DbRelationship) nextPathComponent);
        }

        return true;
    }

    private void breakChain(int index) {
        // strip everything starting from the index
        dbRelationshipPath.makeActive(false);

        try {
            while (dbRelationshipPath.size() > (index + 1)) {
                // remove last
                dbRelationshipPath.remove(dbRelationshipPath.size() - 1);
            }
        }
        finally {
            dbRelationshipPath.makeActive(true);
        }
    }

    // Connects last selected DbRelationship in the path to the 
    // last DbEntity, creating a dummy relationship if needed.
    private void connectEnds() {
        Relationship last = null;

        int size = dbRelationshipPath.size();
        if (size > 0) {
            EntityRelationshipsModel wrapper =
                (EntityRelationshipsModel) dbRelationshipPath.get(size - 1);
            last = wrapper.getSelectedRelationship();

        }

        Entity target = getEndEntity();

        if (last == null || last.getTargetEntity() != target) {
            // try to connect automatically, if we can't use dummy connector

            Entity source = (last == null) ? getStartEntity() : last.getTargetEntity();
            Relationship anyConnector = source.getAnyRelationship(target);
            EntityRelationshipsModel connector = null;

            connector =
                (anyConnector == null)
                    ? new EntityRelationshipsModel(source, getEndEntity())
                    : new EntityRelationshipsModel(anyConnector);

            dbRelationshipPath.makeActive(false);
            try {
                dbRelationshipPath.add(connector);
            }
            finally {
                dbRelationshipPath.makeActive(true);
            }
        }
    }

    private void validateCanMap() {
        if (relationship.getSourceEntity() == null) {
            throw new CayenneRuntimeException("Can't map relationship without source entity.");
        }

        if (relationship.getTargetEntity() == null) {
            throw new CayenneRuntimeException("Can't map relationship without target entity.");
        }

        if (getStartEntity() == null) {
            throw new CayenneRuntimeException("Can't map relationship without source DbEntity.");
        }

        if (getEndEntity() == null) {
            throw new CayenneRuntimeException("Can't map relationship without target DbEntity.");
        }
    }

    public DbEntity getStartEntity() {
        return ((ObjEntity) relationship.getSourceEntity()).getDbEntity();
    }

    public DbEntity getEndEntity() {
        return objectTarget.getDbEntity();
    }
}
