/**
 * This file is part of CloudML [ http://cloudml.org ]
 *
 * Copyright (C) 2012 - SINTEF ICT
 * Contact: Franck Chauvel <franck.chauvel@sintef.no>
 *
 * Module: root
 *
 * CloudML is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * CloudML 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with CloudML. If not, see
 * <http://www.gnu.org/licenses/>.
 */
package org.cloudml.core;

import java.util.Collection;
import org.cloudml.core.collections.ComponentInstanceGroup;
import org.cloudml.core.collections.InternalComponentInstanceGroup;
import org.cloudml.core.util.OwnedBy;
import org.cloudml.core.collections.ProvidedExecutionPlatformInstanceGroup;
import org.cloudml.core.collections.ProvidedPortInstanceGroup;
import org.cloudml.core.validation.Report;

public abstract class ComponentInstance<T extends Component> extends WithResources implements DeploymentElement, OwnedBy<Deployment> {

    public static enum State {

        /**
         * The node is in transition.
         */
        PENDING("PENDING"),
        /**
         * The node is visible, and in the process of being deleted. Available
         * on jCLouds only.
         */
        TERMINATED("TERMINATED"),
        /**
         * The node is deployed, but suspended or stopped.
         */
        STOPPED("STOPPED"),
        /**
         * The node is available for requests
         */
        RUNNING("RUNNING"),
        /**
         * There is an error on the node
         */
        ERROR("ERROR"),
        /**
         * The state of the node is unrecognized.
         */
        UNRECOGNIZED("UNRECOGNIZED"),
        /**
         * This status is available only on Flexiant
         */
        RECOVERY("RECOVERY");

        private String name = "PENDING";

        private State(String s) {
            this.name = s;
        }

        public boolean equalsName(String otherName) {
            return (otherName == null) ? false : name.equals(otherName);
        }

        public String toString() {
            return name;
        }

    }
    private final OptionalOwner<Deployment> owner;
    private T type;
    private ProvidedPortInstanceGroup providedPorts;
    private ProvidedExecutionPlatformInstanceGroup providedExecutionPlatforms;

    public ComponentInstance(String name, T type) {
        super(name);
        this.owner = new OptionalOwner<Deployment>();
        setType(type);
        this.providedPorts = instantiateAllProvidedPorts(type);
        this.providedExecutionPlatforms = instantiateAllExecutionPlatforms(type);
    }

    private ProvidedPortInstanceGroup instantiateAllProvidedPorts(T type) {
        final ProvidedPortInstanceGroup instances = new ProvidedPortInstanceGroup();
        for (ProvidedPort port: type.getProvidedPorts()) {
            instances.add(port.instantiate());
        }
        return new LocalProvidedPortInstanceGroup(instances);
    }

    private LocalProvidedExecutionPlatformInstanceGroup instantiateAllExecutionPlatforms(T type) {
        final ProvidedExecutionPlatformInstanceGroup group = new ProvidedExecutionPlatformInstanceGroup();
        for (ProvidedExecutionPlatform platform: type.getProvidedExecutionPlatforms()) {
            group.add(platform.instantiate());
        }
        return new LocalProvidedExecutionPlatformInstanceGroup(group);
    }

    /**
     * @return true if this component instance can host other instance, (i.e.,
     * if it provide at least one execution platform), false otherwise.
     */
    public boolean canHost() {
        return !getProvidedExecutionPlatforms().isEmpty();
    }

    /**
     * @return true if this component instance can host the given component
     * (i.e., if it provides a relevant execution platform), false otherwise
     * @param componentType the component type that need an host
     */
    public boolean canHost(InternalComponent componentType) {
        return getProvidedExecutionPlatforms().firstMatchFor(componentType) != null;
    }

    public boolean isHosting(InternalComponentInstance component) {
        return hostedComponents().contains(component);
    }

    public InternalComponentInstanceGroup clientComponents() {
        if (getOwner().isUndefined()) {
            return new InternalComponentInstanceGroup();
        }
        return getDeployment().getRelationshipInstances().clientsOf(this);
    }

    public InternalComponentInstanceGroup hostedComponents() {
        if (getOwner().isUndefined()) {
            return new InternalComponentInstanceGroup();
        }
        return getDeployment().getExecuteInstances().componentsHostedBy(this);
    }

    @Override
    public void validate(Report report) {
        if (owner.isUndefined()) {
            final String error = String.format("Component instance '%s' has no owner", getQualifiedName());
            report.addError(error);
        }
    }

    public boolean canBeUninstalled() {
        return !isUsed() && isInternal();
    }

    public boolean isUsed() {
        return !hostedComponents().isEmpty() || !clientComponents().isEmpty();
    }

    public final boolean isInternal() {
        return type.isInternal();
    }

    @SuppressWarnings("unchecked")
    public final InternalComponentInstance asInternal() {
        if (isExternal()) {
            throw new IllegalStateException("Unable to convert an external component instance into an internal one");
        }
        return (InternalComponentInstance) this;
    }

    public final boolean isExternal() {
        return !isInternal();
    }

    @SuppressWarnings("unchecked")
    public final ExternalComponentInstance<ExternalComponent> asExternal() {
        if (isInternal()) {
            throw new IllegalStateException("Unable to convert an internal component instance into an external one");
        }
        return (ExternalComponentInstance<ExternalComponent>) this;
    }

    @Override
    public Deployment getDeployment() {
        return owner.get();
    }

    @Override
    public String getQualifiedName() {
        return getOwner().getName() + "::" + getName();
    }

    @Override
    public OptionalOwner<Deployment> getOwner() {
        return this.owner;
    }

    public ProvidedPortInstanceGroup getProvidedPorts() {
        return this.providedPorts;
    }

    public void setProvidedPorts(ProvidedPortInstanceGroup ppig) {
        this.providedPorts = ppig;
    }

    public T getType() {
        return this.type;
    }

    public final void setType(T type) {
        if (type == null) {
            throw new IllegalArgumentException("'null' is not a valid type for a component instance");
        }
        this.type = type;
    }

    public ProvidedExecutionPlatformInstanceGroup getProvidedExecutionPlatforms() {
        return providedExecutionPlatforms;
    }

    public void setProvidedExecutionPlatforms(ProvidedExecutionPlatformInstanceGroup pepig) {
        this.providedExecutionPlatforms = pepig;
    }

    @Override
    public String toString() {
        return "Instance " + getName() + " : " + getType().getName();
    }

    @Override
    public boolean equals(Object other) {
        if (other == null) {
            return false;
        }

        if (other instanceof ComponentInstance) {
            ComponentInstance otherCompInst = (ComponentInstance) other;
            Boolean match = getName().equals(otherCompInst.getName()) && type.equals(otherCompInst.getType());
            return match;
        } else {
            return false;
        }
    }

    private class LocalProvidedPortInstanceGroup extends ProvidedPortInstanceGroup {

        public LocalProvidedPortInstanceGroup(Collection<ProvidedPortInstance> content) {
            super();
            for (ProvidedPortInstance port: content) {
                super.add(port);
                port.getOwner().set(ComponentInstance.this);
            }
        }

        @Override
        public boolean add(ProvidedPortInstance e) {
            throw new UnsupportedOperationException("The provided ports of a component instance cannot be changed");
        }

        @Override
        public boolean remove(Object o) {
            throw new UnsupportedOperationException("The provided ports of a component instance cannot be changed");
        }

        @Override
        public void clear() {
            throw new UnsupportedOperationException("The provided ports of a component instance cannot be changed");
        }
    }

    private class LocalProvidedExecutionPlatformInstanceGroup extends ProvidedExecutionPlatformInstanceGroup {

        public LocalProvidedExecutionPlatformInstanceGroup(Collection<ProvidedExecutionPlatformInstance> content) {
            super();
            for (ProvidedExecutionPlatformInstance platform: content) {
                super.add(platform);
                platform.getOwner().set(ComponentInstance.this);
            }
        }

        @Override
        public boolean add(ProvidedExecutionPlatformInstance e) {
            throw new UnsupportedOperationException("The provided execution platforms of a component instance cannot be changed");
        }

        @Override
        public boolean remove(Object o) {
            throw new UnsupportedOperationException("The provided execution platforms of a component instance cannot be changed");
        }

        @Override
        public void clear() {
            throw new UnsupportedOperationException("The provided execution platforms of a component instance cannot be changed");
        }
    }
}
