/*
 * JBoss, Home of Professional Open Source
 * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the @author tags. All rights reserved.
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License, v. 2.1.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License,
 * v.2.1 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */
package org.jboss.as.console.client.shared.subsys.infinispan;

import org.jboss.as.console.client.Console;
import org.jboss.as.console.client.shared.properties.PropertyRecord;
import org.jboss.as.console.client.shared.subsys.Baseadress;
import org.jboss.as.console.client.shared.subsys.infinispan.model.LocalCache;
import org.jboss.as.console.client.shared.subsys.infinispan.model.ReplicatedCache;
import org.jboss.as.console.client.shared.viewframework.DmrCallback;
import org.jboss.as.console.client.shared.viewframework.EntityToDmrBridgeImpl;
import org.jboss.as.console.client.shared.viewframework.FrameworkView;
import org.jboss.as.console.client.widgets.forms.ApplicationMetaData;
import org.jboss.as.console.client.widgets.forms.PropertyBinding;
import org.jboss.dmr.client.ModelNode;
import org.jboss.dmr.client.Property;
import org.jboss.dmr.client.dispatch.DispatchAsync;
import org.jboss.dmr.client.dispatch.impl.DMRAction;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.jboss.dmr.client.ModelDescriptionConstants.*;

/**
 * This EntityToDmrBridge has special logic to handle the singleton-object pattern used with the cache
 * models.  It also has logic to set a cache as the default container if requested.
 *
 * @author Stan Silvert ssilvert@redhat.com (C) 2011 Red Hat Inc.
 */
public class CacheEntityToDmrBridge<T extends LocalCache> extends EntityToDmrBridgeImpl<T> {

    private static final Set<String> singletons = new HashSet<String>();
    private static final Set<String> addRemoveSingletonCheckboxes = new HashSet<String>();

    static {
        singletons.add("store");
        singletons.add("locking");
        singletons.add("eviction");
        singletons.add("expiration");
        singletons.add("transaction");
        singletons.add("file-store");
        singletons.add("remote-store");
        singletons.add("jdbc-store");
        singletons.add("state-transfer");

        addRemoveSingletonCheckboxes.add("hasStore");
        addRemoveSingletonCheckboxes.add("hasLocking");
        addRemoveSingletonCheckboxes.add("hasEviction");
        addRemoveSingletonCheckboxes.add("hasExpiration");
        addRemoveSingletonCheckboxes.add("hasTransaction");
        addRemoveSingletonCheckboxes.add("hasFileStore");
        addRemoveSingletonCheckboxes.add("hasRemoteStore");
        addRemoveSingletonCheckboxes.add("hasJdbcStore");
        addRemoveSingletonCheckboxes.add("hasStateTransfer");
    }

    public CacheEntityToDmrBridge(ApplicationMetaData propertyMetadata, Class<? extends T> type, FrameworkView view,
                                 DispatchAsync dispatcher) {
        super(propertyMetadata, type, view, dispatcher);
    }

    @Override
    public String getName(T entity) {
        return  entity.getCacheContainer() + "/" + entity.getName();
    }

    @Override
    protected ModelNode getResourceAddress(String name) {
        String[] compoundName = name.split("/");
        return address.asResource(Baseadress.get(), compoundName[0], compoundName[1]);
    }

    @Override
    public void loadEntities(String nameEditedOrAdded) {
        this.nameOfLastEdited = nameEditedOrAdded;

        ModelNode steps = new ModelNode();
        steps.get(OP).set(COMPOSITE);

        // get all the cache info
        ModelNode readRscOp = address.asResource(Baseadress.get(), "*", "*");
        readRscOp.get(OP).set(READ_RESOURCE_OPERATION);
        readRscOp.get(RECURSIVE).set(true);
        steps.get(STEPS).add(readRscOp);

        // for each container, find the default cache
        ModelNode readAttrOp = new ModelNode();
        readAttrOp.get(ADDRESS).set(Baseadress.get()).add("subsystem", "infinispan").add("cache-container", "*");
        readAttrOp.get(OP).set(READ_ATTRIBUTE_OPERATION);
        readAttrOp.get(NAME).set("default-cache");
        steps.get(STEPS).add(readAttrOp);

   //     System.out.println("load entities for " + this.type.getName());
   //     System.out.println(steps.toString());

        dispatcher.execute(new DMRAction(steps), new DmrCallback() {
            @Override
            public void onDmrSuccess(ModelNode response) {
        //       System.out.println("loadEntities response=");
        //       System.out.println(response.toString());
               onLoadEntitiesSuccess(response);
            }
        });

    }

    @Override
    protected void onLoadEntitiesSuccess(ModelNode response) {
        List<T> entities = new ArrayList<T>();

        for (ModelNode entity : response.get(RESULT).get("step-1").get(RESULT).asList()) {
            ModelNode result = entity.get(RESULT);
            for (Property addressProp : entity.get(ADDRESS).asPropertyList()) {
                result.get(addressProp.getName()).set(addressProp.getValue());
            }

            boolean eviction = result.get("eviction").isDefined();
            boolean expiration = result.get("expiration").isDefined();
            boolean locking = result.get("locking").isDefined();
            boolean store = result.get("store").isDefined();
            boolean file = result.get("file-store").isDefined();
            boolean remote = result.get("remote-store").isDefined();
            boolean jdbc = result.get("jdbc-store").isDefined();
            boolean transaction = result.get("transaction").isDefined();

            T cache = entityAdapter.fromDMR(result);

            cache.setHasEviction(eviction);
            cache.setHasExpiration(expiration);
            cache.setHasLocking(locking);
            cache.setHasStore(store);
            cache.setHasFileStore(file);
            cache.setHasRemoteStore(remote);
            cache.setHasJdbcStore(jdbc);
            cache.setHasTransaction(transaction);

            if (cache instanceof ReplicatedCache) {
                ReplicatedCache repl = (ReplicatedCache)cache;
                repl.setHasStateTransfer(result.get("state-transfer").isDefined());
            }

            entities.add(cache);
        }

        entityList = sortEntities(entities);

        setIsDefaultCacheAttribute(response.get(RESULT).get("step-2"));

        refreshView(response);
    }

    private void setIsDefaultCacheAttribute(ModelNode response) {
        Set<String> defaultCaches = new HashSet<String>();
        for (ModelNode defaultCache : response.get(RESULT).asList()) {
            List<ModelNode> addressList = defaultCache.get(ADDRESS).asList();
            String cacheContainer = addressList.get(addressList.size() - 1).get("cache-container").asString();
            String cache = defaultCache.get(RESULT).asString();
            defaultCaches.add(cacheContainer + "/" + cache);
        }

        for (T entity : entityList) {
            entity.setDefault(defaultCaches.contains(getName(entity)));
        }
    }

    @Override
    public void onSaveDetails(T entity, Map<String, Object> changedValues, ModelNode... extraSteps) {
        List<ModelNode> stepsList = new ArrayList<ModelNode>();
        stepsList.addAll(Arrays.asList(extraSteps));

        ModelNode setDefaultCacheStep = makeSetDefaultCacheStep(entity, changedValues);
        if ((setDefaultCacheStep != null) && setDefaultCacheStep.get("ERROR").isDefined()) return;
        if (setDefaultCacheStep != null) stepsList.add(setDefaultCacheStep);

        List<ModelNode> singletonEntitySteps = makeSingletonEntitySteps(entity, changedValues);
        stepsList.addAll(singletonEntitySteps);

        super.onSaveDetails(entity, changedValues, stepsList.toArray(new ModelNode[stepsList.size()]));
    }

    private List<ModelNode> makeSingletonEntitySteps(T entity, Map<String, Object> changedValues) {
        List<ModelNode> stepsList = new ArrayList<ModelNode>();

        String addRemoveAttrib = findAddRemoveSingletonAttribute(changedValues);
        if ((addRemoveAttrib != null) && (!(Boolean)changedValues.get(addRemoveAttrib))) { // removing singleton
            stepsList.add(makeAddOrRemoveSingletonStep(entity, singletonName(addRemoveAttrib), REMOVE));
            removeSingletonAttributes(changedValues);
            return stepsList;
        }

        if (addRemoveAttrib != null) { // adding singleton
            changedValues.remove(addRemoveAttrib); // remove the add singleton flag, which is not written to DMR model
            ModelNode addSingleton = makeAddOrRemoveSingletonStep(entity, singletonName(addRemoveAttrib), ADD);

            if (addRemoveAttrib.equals("hasStore")) {
                String storeImplClass = (String)changedValues.remove("storeClass");
                if (storeImplClass == null) {
                    Console.error("Must specify Store Impl Class when defining Store.");
                    removeSingletonAttributes(changedValues);
                    return stepsList;
                }
                addSingleton.get("class").set(storeImplClass);
            }

            if (addRemoveAttrib.equals("hasJdbcStore")) {
                String datasource = (String)changedValues.remove("jdbcStoreDatasource");
                if (datasource == null) {
                    Console.error("Must specify datasource when defining JDBC Store.");
                    removeSingletonAttributes(changedValues);
                    return stepsList;
                }
                addSingleton.get("datasource").set(datasource);
            }

            stepsList.add(addSingleton);
        }

        for (String javaName : changedValues.keySet()) {
            String singletonName = singletonName(javaName);
            if (!singletons.contains(singletonName)) continue;

            ModelNode writeSingletonAttributeStep = getResourceAddress(getName(entity));
            writeSingletonAttributeStep.get(ADDRESS).add(singletonName, singletonName.toUpperCase().replace('-', '_'));
            writeSingletonAttributeStep.get(OP).set(WRITE_ATTRIBUTE_OPERATION);
            writeSingletonAttributeStep.get(NAME).set(attributeName(javaName));

            if (this.formMetaData.findAttribute(javaName).getListType() != null
                    && (changedValues.get(javaName) instanceof List)) {
                for (PropertyRecord prop : (List<PropertyRecord>)changedValues.get(javaName)) {
                    writeSingletonAttributeStep.get(VALUE).add(prop.getKey(), prop.getValue());
                }
            } else {
                writeSingletonAttributeStep.get(VALUE).set(changedValues.get(javaName).toString());
            }
            stepsList.add(writeSingletonAttributeStep);
        }

        // Need to remove singleton attributes from normal processing.
        removeSingletonAttributes(changedValues);

        return stepsList;
    }

    // Find the request to add or remove a singleton
    private String findAddRemoveSingletonAttribute(Map<String, Object> changedValues) {
        for (Map.Entry<String, Object> entry : changedValues.entrySet()) {
            if (addRemoveSingletonCheckboxes.contains(entry.getKey())) {
                return entry.getKey();
            }
        }

        return null;
    }

    // If singleton attribute, such as "/transaction/TRANSACTION/foo-attribute",
    // singleton name will be the first name in the path.
    private String singletonName(String javaName) {
        PropertyBinding binding = formMetaData.findAttribute(javaName);
        String[] splitDetypedName = binding.getDetypedName().split("/");
        return splitDetypedName[0];
    }

    // If singleton attribute, such as "/transaction/TRANSACTION/foo-attribute",
    // singleton name will be the last name in the path.
    private String attributeName(String javaName) {
        PropertyBinding binding = formMetaData.findAttribute(javaName);
        String[] splitDetypedName = binding.getDetypedName().split("/");
        return splitDetypedName[splitDetypedName.length - 1];
    }

    // remove all singleton attributes from changedValues so they are not
    // processed by the superclass
    private void removeSingletonAttributes(Map<String, Object> changedValues) {
        List<String> removals = new ArrayList<String>();
        for (String javaName : changedValues.keySet()) {
            String singletonName = singletonName(javaName);
            if (!singletons.contains(singletonName)) continue;
            removals.add(javaName);
        }

        // remove outside the loop to avoid ConcurrentModificationException
        for (String removal : removals) {
            changedValues.remove(removal);
        }
    }

    // add or remove a singleton in the model.  operation param will == ADD or REMOVE.
    private ModelNode makeAddOrRemoveSingletonStep(T entity, String singletonName, String operation) {
        ModelNode addOrRemoveSingletonStep = getResourceAddress(getName(entity));
        addOrRemoveSingletonStep.get(ADDRESS).add(singletonName, singletonName.toUpperCase().replace('-', '_'));
        addOrRemoveSingletonStep.get(OP).set(operation);
        return addOrRemoveSingletonStep;
    }

    // If user asks a cache to become the default for the cache container, set it as default in the cache-container's
    // model.
    private ModelNode makeSetDefaultCacheStep(T entity, Map<String, Object> changedValues) {
        Boolean isDefault = (Boolean)changedValues.remove("default");
        if (isDefault == null) {
            return null;
        }

        ModelNode setDefaultCacheStep = new ModelNode();

        if (entity.isDefault()) {
            Console.error("Can not unset 'Default for cache container'.  Instead, set a different cache to be the default.");
            setDefaultCacheStep.get("ERROR").set("ERROR");
            return setDefaultCacheStep;
        }

        String cacheContainer = entity.getCacheContainer();

        setDefaultCacheStep.get(ADDRESS).set(Baseadress.get()).add("subsystem", "infinispan").add("cache-container", cacheContainer);
        setDefaultCacheStep.get(OP).set(WRITE_ATTRIBUTE_OPERATION);
        setDefaultCacheStep.get(NAME).set("default-cache");
        setDefaultCacheStep.get(VALUE).set(entity.getName());

        return setDefaultCacheStep;
    }

}
