/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.controller.transform.description;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.registry.Resource;
import org.jboss.as.controller.registry.Resource.ResourceEntry;
import org.jboss.as.controller.transform.ChainedTransformationTools;
import org.jboss.as.controller.transform.OperationResultTransformer;
import org.jboss.as.controller.transform.OperationTransformer;
import org.jboss.as.controller.transform.PathAddressTransformer;
import org.jboss.as.controller.transform.ResourceTransformationContext;
import org.jboss.as.controller.transform.ResourceTransformer;
import org.jboss.as.controller.transform.TransformationContext;
import org.jboss.as.controller.transform.description.ChainedTransformationDescriptionBuilderImpl.ModelVersionPair;
import org.jboss.dmr.ModelNode;

/**
 * Placeholder transformer implementation for chained transformers. It uses {@link org.jboss.as.controller.registry.OperationTransformerRegistry.PlaceholderResolver} to override how the transformers for the child resources
 * of the placeholder are resolved.
 *
 *
 * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
 */
class ChainedTransformingDescription extends AbstractDescription implements TransformationDescription, ResourceTransformer, OperationTransformer {

    private final LinkedHashMap<ModelVersionPair, ChainedPlaceholderResolver> placeholderResolvers;

    ChainedTransformingDescription(PathElement pathElement, LinkedHashMap<ModelVersionPair, ChainedPlaceholderResolver> placeholderResolvers) {
        super(pathElement, PathAddressTransformer.DEFAULT, true);
        this.placeholderResolvers = placeholderResolvers;
    }

    @Override
    public TransformedOperation transformOperation(TransformationContext context, PathAddress address, ModelNode operation)
            throws OperationFailedException {
        assert context instanceof ResourceTransformationContext : "Wrong context type";
        ResourceTransformationContext currentCtx = (ResourceTransformationContext)context;

        List<TransformedOperation> delegates = new ArrayList<TransformedOperation>();
        ModelNode currentOp = operation;
        Iterator<Map.Entry<ModelVersionPair, ChainedPlaceholderResolver>> it = placeholderResolvers.entrySet().iterator();
        if (it.hasNext()) {
            ChainedPlaceholderResolver resolver = it.next().getValue();
            currentCtx = ChainedTransformationTools.initialiseChain(currentCtx, resolver);
            PathAddress addr = ChainedTransformationTools.transformAddress(PathAddress.pathAddress(currentOp.require(OP_ADDR)), currentCtx.getTarget());
            currentOp.get(OP_ADDR).set(addr.toModelNode());
            OperationTransformer transformer = currentCtx.getTarget().resolveTransformer(currentCtx, address, currentOp.require(OP).asString());
            TransformedOperation transformed = transformer.transformOperation(currentCtx, address, currentOp);
            currentOp = transformed.getTransformedOperation();
            delegates.add(transformed);
        }
        while (it.hasNext()) {
            if (currentOp == null) {
                break;
            }
            ChainedPlaceholderResolver resolver = it.next().getValue();
            currentCtx = ChainedTransformationTools.nextInChainOperation(currentCtx, resolver);
            PathAddress currentAddress = PathAddress.pathAddress(currentOp.require(OP_ADDR));
            PathAddress addr = ChainedTransformationTools.transformAddress(currentAddress, currentCtx.getTarget());
            currentOp.get(OP_ADDR).set(addr.toModelNode());
            OperationTransformer transformer = currentCtx.getTarget().resolveTransformer(currentCtx, currentAddress, currentOp.require(OP).asString());
            TransformedOperation transformed = transformer.transformOperation(currentCtx, address, currentOp);
            currentOp = transformed.getTransformedOperation();
            delegates.add(transformed);
        }

        return new ChainedTransformedOperation(currentOp, delegates.toArray(new TransformedOperation[delegates.size()]));
    }

    @Override
    public void transformResource(final ResourceTransformationContext context, final PathAddress address, final Resource resource) throws OperationFailedException {
        if (resource.isProxy() || resource.isRuntime()) {
            return;
        }

        //For now just assume we come in through the top layer
        ResourceTransformationContext current = context;
        Iterator<Map.Entry<ModelVersionPair, ChainedPlaceholderResolver>> it = placeholderResolvers.entrySet().iterator();
        if (it.hasNext()) {
            ChainedPlaceholderResolver resolver = it.next().getValue();
            current = ChainedTransformationTools.initialiseChain(current, resolver);
            resolver.getDescription().getResourceTransformer().transformResource(current, address, resource);
        }
        while (it.hasNext()) {
            ChainedPlaceholderResolver resolver = it.next().getValue();
            current = ChainedTransformationTools.nextInChainResource(current, resolver);
            try {
                Resource currentResource = current.readResourceFromRoot(address);
                resolver.getDescription().getResourceTransformer().transformResource(current, address, currentResource);
            } catch (Resource.NoSuchResourceException e) {
                //The resource was rejected/discarded
                continue;
            }
        }

        Resource transformed = current.getTransformedRoot();
        Resource originalTransformed = context.getTransformedRoot();
        copy(transformed, originalTransformed, address);
    }


    @Override
    public PathAddressTransformer getPathAddressTransformer() {
        return PathAddressTransformer.DEFAULT;
    }

    @Override
    public OperationTransformer getOperationTransformer() {
        return this;
    }

    @Override
    public ResourceTransformer getResourceTransformer() {
        return this;
    }

    @Override
    public Map<String, OperationTransformer> getOperationTransformers() {
        return Collections.emptyMap();
    }

    @Override
    public List<TransformationDescription> getChildren() {
        return Collections.emptyList();
    }

    @Override
    public Set<String> getDiscardedOperations() {
        return Collections.emptySet();
    }

    @Override
    public boolean isPlaceHolder() {
        return true;
    }

    private void copy(Resource src, Resource dest, PathAddress address) {
        PathAddress parentAddress = address.size() > 1 ? address.subAddress(0, address.size() - 1) : PathAddress.EMPTY_ADDRESS;
        PathElement childElement = address.getLastElement();
        Resource source = src.navigate(parentAddress);
        Resource destination = dest.navigate(parentAddress);
        Resource sourceChild = source.getChild(childElement);
        if (sourceChild != null) {
            Resource destChild = Resource.Factory.create();
            destination.registerChild(childElement, destChild);
            copy(sourceChild, destChild);
        }
        //copy(src, dest);
    }

    private void copy(Resource src, Resource dest) {
        dest.getModel().set(src.getModel());
        for (String type : src.getChildTypes()) {
            for (ResourceEntry entry : src.getChildren(type)) {
                Resource added = Resource.Factory.create();
                dest.registerChild(PathElement.pathElement(type, entry.getName()), added);
                copy(entry, added);
            }
        }
    }

    private PathAddress transformAddress(final PathAddress original, final ResourceTransformationContext context) {
        return ChainedTransformationTools.transformAddress(original, context.getTarget());
    }

    private static class ChainedTransformedOperation extends TransformedOperation {

        private TransformedOperation[] delegates;
        private volatile String failure;
        private volatile boolean initialized;

        public ChainedTransformedOperation(ModelNode transformedOperation, TransformedOperation...delegates) {
            // FIXME ChainedTransformedOperation constructor
            super(transformedOperation, null);
            this.delegates = delegates;
        }

        @Override
        public ModelNode getTransformedOperation() {
            return super.getTransformedOperation();
        }

        @Override
        public OperationResultTransformer getResultTransformer() {
            return this;
        }

        @Override
        public boolean rejectOperation(ModelNode preparedResult) {
            for (TransformedOperation delegate : delegates) {
                if (delegate.rejectOperation(preparedResult)) {
                    failure = delegate.getFailureDescription();
                    initialized = true; //See comment in getFailureDescription()
                    return true;
                }
            }
            return false;
        }

        @Override
        public String getFailureDescription() {
            //In real life this will always be initialized by the transforming proxy before anyone calls this method
            //For testing we call it directly from ModelTestUtils
            if (!initialized) {
                for (TransformedOperation delegate : delegates) {
                    String failure = delegate.getFailureDescription();
                    if (failure != null) {
                        return failure;
                    }
                }
            }
            return failure;
        }

        @Override
        public ModelNode transformResult(ModelNode result) {
            ModelNode currentResult = result;
            for (int i = delegates.length - 1 ; i >= 0 ; --i) {
                currentResult = delegates[i].transformResult(currentResult);
            }
            return currentResult;
        }
    }
}
