/*
 * 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.brooklyn.location.jclouds;

import static org.apache.brooklyn.util.JavaGroovyEquivalents.groovyTruth;

import java.net.InetAddress;
import java.util.Set;

import org.apache.brooklyn.location.winrm.WinRmMachineLocation;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.net.Networking;
import org.jclouds.compute.domain.Image;
import org.jclouds.compute.domain.NodeMetadata;
import org.jclouds.compute.domain.Template;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableSet;

public class JcloudsWinRmMachineLocation extends WinRmMachineLocation implements JcloudsMachineLocation {

    private static final Logger LOG = LoggerFactory.getLogger(JcloudsWinRmMachineLocation.class);

    @SetFromFlag
    JcloudsLocation jcloudsParent;
    
    /**
     * @deprecated since 0.9.0; the node should not be persisted.
     */
    @SetFromFlag
    @Deprecated
    NodeMetadata node;
    
    /**
     * @deprecated since 0.9.0; the template should not be persisted.
     */
    @SetFromFlag
    @Deprecated
    Template template;
    
    @SetFromFlag
    String nodeId;

    @SetFromFlag
    String imageId;

    @SetFromFlag
    Set<String> privateAddresses;
    
    @SetFromFlag
    Set<String> publicAddresses;

    @SetFromFlag
    String hostname;

    /**
     * Historically, "node" and "template" were persisted. However that is a very bad idea!
     * It means we pull in lots of jclouds classes into the persisted state. We are at an  
     * intermediate stage, where we want to handle rebinding to old state that has "node"
     * and new state that should not. We therefore leave in the {@code @SetFromFlag} on node
     * so that we read it back, but we ensure the value is null when we write it out!
     * 
     * TODO We will rename these to get rid of the ugly underscore when the old node/template 
     * fields are deleted.
     */
    private transient Optional<NodeMetadata> _node;

    private transient Optional<Template> _template;

    private transient Optional<Image> _image;

    private transient String _privateHostname;
    
    public JcloudsWinRmMachineLocation() {
    }

    @Override
    public void init() {
        if (jcloudsParent != null) {
            super.init();
            if (node != null) {
                setNode(node);
            }
            if (template != null) {
                setTemplate(template);
            }
        } else {
            // TODO Need to fix the rebind-detection, and not call init() on rebind.
            // This will all change when locations become entities.
            if (LOG.isDebugEnabled()) LOG.debug("Not doing init() of {} because parent not set; presuming rebinding", this);
        }
    }
    
    @Override
    public void rebind() {
        super.rebind();
        
        if (node != null) {
            setNode(node);
            node = null;
        }

        if (template != null) {
            setTemplate(template);
            template = null;
        }
    }
    
    @Override
    public String toVerboseString() {
        return Objects.toStringHelper(this).omitNullValues()
                .add("id", getId()).add("name", getDisplayName())
                .add("user", getUser())
                .add("address", getAddress())
                .add("port", getPort())
                .add("node", _node)
                .add("nodeId", getJcloudsId())
                .add("imageId", getImageId())
                .add("privateAddresses", getPrivateAddresses())
                .add("publicAddresses", getPublicAddresses())
                .add("parentLocation", getParent())
                .add("osDetails", getOsDetails())
                .toString();
    }

    protected void setNode(NodeMetadata node) {
        this.node = null;
        nodeId = node.getId();
        imageId = node.getImageId();
        privateAddresses = node.getPrivateAddresses();
        publicAddresses = node.getPublicAddresses();
        hostname = node.getHostname();
        _node = Optional.of(node);
    }

    protected void setTemplate(Template template) {
        this.template = null;
        _template = Optional.of(template);
        _image = Optional.fromNullable(template.getImage());
    }

    @Override
    public JcloudsLocation getParent() {
        return jcloudsParent;
    }
    
    @Override
    public Optional<NodeMetadata> getOptionalNode() {
      if (_node == null) {
          _node = Optional.fromNullable(getParent().getComputeService().getNodeMetadata(nodeId));
      }
      return _node;
    }

    @VisibleForTesting
    Optional<NodeMetadata> peekNode() {
        return _node;
    }

    protected Optional<Image> getOptionalImage() {
        if (_image == null) {
            _image = Optional.fromNullable(getParent().getComputeService().getImage(imageId));
        }
        return _image;
    }

    /**
     * @since 0.9.0
     * @deprecated since 0.9.0 (only added as aid until the deprecated {@link #getTemplate()} is deleted)
     */
    @Deprecated
    protected Optional<Template> getOptionalTemplate() {
        if (_template == null) {
            _template = Optional.absent();
        }
        return _template;
    }

    /**
     * @deprecated since 0.9.0
     */
    @Override
    @Deprecated
    public NodeMetadata getNode() {
        Optional<NodeMetadata> result = getOptionalNode();
        if (result.isPresent()) {
            return result.get();
        } else {
            throw new IllegalStateException("Node "+nodeId+" not present in "+getParent());
        }
    }

    /**
     * @deprecated since 0.9.0
     */
    @Override
    @Deprecated
    public Template getTemplate() {
        Optional<Template> result = getOptionalTemplate();
        if (result.isPresent()) {
            String msg = "Deprecated use of getTemplate() for "+this;
            LOG.warn(msg + " - see debug log for stacktrace");
            LOG.debug(msg, new Exception("for stacktrace"));
            return result.get();
        } else {
            throw new IllegalStateException("Template for "+nodeId+" (in "+getParent()+") not cached (deprecated use of getTemplate())");
        }
    }

    @Override
    public String getHostname() {
        if (hostname != null) {
            return hostname;
        }
        InetAddress address = getAddress();
        return (address != null) ? address.getHostAddress() : null;
    }


    /** In clouds like AWS, the public hostname is the only way to ensure VMs in different zones can access each other. */
    @Override
    public Set<String> getPublicAddresses() {
        return (publicAddresses == null) ? ImmutableSet.<String>of() : publicAddresses;
    }
    
    @Override
    public Set<String> getPrivateAddresses() {
        return (privateAddresses == null) ? ImmutableSet.<String>of() : privateAddresses;
    }


    @Override
    public String getSubnetHostname() {
        // Same impl as JcloudsSshMachineLocation
        if (_privateHostname == null) {
            for (String p : getPrivateAddresses()) {
                if (Networking.isLocalOnly(p)) continue;
                _privateHostname = p;
            }
            if (groovyTruth(getPublicAddresses())) {
                _privateHostname = getPublicAddresses().iterator().next();
            } else if (groovyTruth(getHostname())) {
                _privateHostname = getHostname();
            } else {
                return null;
            }
        }
        return _privateHostname;
    }

    @Override
    public String getSubnetIp() {
        // Same impl as JcloudsSshMachineLocation
        Optional<String> privateAddress = getPrivateAddress();
        if (privateAddress.isPresent()) {
            return privateAddress.get();
        }
        if (groovyTruth(node.getPublicAddresses())) {
            return node.getPublicAddresses().iterator().next();
        }
        return null;
    }

    protected Optional<String> getPrivateAddress() {
        // Same impl as JcloudsSshMachineLocation
        for (String p : getPrivateAddresses()) {
            if (Networking.isLocalOnly(p)) continue;
            return Optional.of(p);
        }
        return Optional.absent();
    }
    
    @Override
    public String getJcloudsId() {
        return nodeId;
    }
    
    protected String getImageId() {
        return imageId;
    }
}
