/*
 * 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.qa.load;

import java.net.URI;
import java.util.concurrent.Callable;

import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.entity.BrooklynConfigKeys;
import org.apache.brooklyn.core.entity.lifecycle.ServiceStateLogic.ServiceNotUpLogic;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcess;
import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcessImpl;
import org.apache.brooklyn.entity.software.base.VanillaSoftwareProcessSshDriver;
import org.apache.brooklyn.feed.function.FunctionFeed;
import org.apache.brooklyn.feed.function.FunctionPollConfig;
import org.apache.brooklyn.feed.http.HttpFeed;
import org.apache.brooklyn.feed.http.HttpPollConfig;
import org.apache.brooklyn.feed.http.HttpValueFunctions;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.time.Duration;

/**
 * For simulating various aspects of the {@link VanillaSoftwareProcess} entity.
 *  
 * It is assumed that the ssh commands for install, launch, etc will be written with testability in mind.
 * For example, they might just be {@code echo} statements, because there is insufficient resources to 
 * run 100s of processes.
 * 
 * It is thus possible to simulate aspects of the behaviour, for performance and load testing purposes. 
 * 
 * There is configuration for:
 * <ul>
 *   <li>{@code skipSshOnStart}
 *     <ul>
 *       <li>If true, then no ssh commands will be executed at deploy-time. 
 *           This is useful for speeding up load testing, to get to the desired number of entities.
 *       <li>If false, the ssh commands will be executed.
 *     </ul>
 *   <li>{@code simulateEntity}
 *     <ul>
 *       <li>if true, no underlying entity will be started. Instead a sleep 100000 job will be run and monitored.
 *       <li>if false, the underlying entity (i.e. a JBoss app-server) will be started as normal.
 *     </ul>
 *   <li>{@code simulateExternalMonitoring}
 *     <ul>
 *       <li>if true, disables the default monitoring mechanism. Instead, a function will periodically execute 
 *           to set the entity's sensors (as though the values had been obtained from the external monitoring tool).
 *       <li>if false, then:
 *         <ul>
 *           <li>If {@code simulateEntity==true} it will execute comparable commands (e.g. execute a command of the same 
 *               size over ssh or do a comparable number of http GET requests).
 *           <li>If {@code simulateEntity==false} then normal monitoring will be done.
 *         </ul>
 *     </ul>
 * </ul>
 */
public class SimulatedVanillaSoftwareProcessImpl extends VanillaSoftwareProcessImpl {

    public static final ConfigKey<Boolean> EXEC_SSH_ON_START = ConfigKeys.newBooleanConfigKey(
            "execSshOnStart", 
            "If true, will execute the ssh commands on install/launch; if false, will skip them", 
            true);

    public static final ConfigKey<URI> HTTP_FEED_URI = ConfigKeys.newConfigKey(
            URI.class, 
            "httpFeed.uri", 
            "If non-null, the URI to poll periodically using a HttpFeed", null);

    public static final ConfigKey<Duration> HTTP_FEED_POLL_PERIOD = ConfigKeys.newConfigKey(
            Duration.class, 
            "httpFeed.pollPeriod", 
            "The poll priod for the HttpFeed (if 'httpFeed.uri' was non-null)", 
            Duration.ONE_SECOND);

    public static final ConfigKey<Duration> FUNCTION_FEED_POLL_PERIOD = ConfigKeys.newConfigKey(
            Duration.class, 
            "functionFeed.pollPeriod", 
            "The poll priod for a function that increments 'counter' periodically (if null, then no-op)", 
            Duration.ONE_SECOND);

    // see SERVICE_PROCESS_IS_RUNNING_POLL_PERIOD
    // Inspired by EmptySoftwareProcess.USE_SSH_MONITORING
    public static final ConfigKey<Boolean> USE_SSH_MONITORING = ConfigKeys.newConfigKey(
            "sshMonitoring.enabled", 
            "Whether to poll periodically over ssh, using the driver.isRunning check", 
            Boolean.TRUE);

    private static final AttributeSensor<String> HTTP_STRING_ATTRIBUTE = Sensors.newStringSensor("httpStringAttribute");

    private static final AttributeSensor<Integer> HTTP_INT_ATTRIBUTE = Sensors.newIntegerSensor("httpIntAttribute");

    private static final AttributeSensor<Long> FUNCTION_COUNTER = Sensors.newLongSensor("functionCounter");

    private FunctionFeed functionFeed;
    private HttpFeed httpFeed;
    
    @Override
    public void init() {
        super.init();
        if (Boolean.FALSE.equals(config().get(EXEC_SSH_ON_START))) {
            config().set(BrooklynConfigKeys.SKIP_ON_BOX_BASE_DIR_RESOLUTION, true);
        }
    }
    
    @Override
    public Class<?> getDriverInterface() {
        return SimulatedVanillaSoftwareProcessSshDriver.class;
    }

    @Override
    protected void connectServiceUpIsRunning() {
        boolean useSshMonitoring = Boolean.TRUE.equals(config().get(USE_SSH_MONITORING));
        if (useSshMonitoring) {
            super.connectServiceUpIsRunning();
        }
    }
    
    @Override
    protected void initEnrichers() {
        super.initEnrichers();
    }
    
    @Override
    protected void connectSensors() {
        super.connectSensors();
        
        boolean useSshMonitoring = Boolean.TRUE.equals(config().get(USE_SSH_MONITORING));
        Duration functionFeedPeriod = config().get(FUNCTION_FEED_POLL_PERIOD);
        URI httpFeedUri = config().get(HTTP_FEED_URI);
        
        if (!useSshMonitoring) {
            ServiceNotUpLogic.clearNotUpIndicator(this, SERVICE_PROCESS_IS_RUNNING);
        }

        if (functionFeedPeriod != null) {
            functionFeed = feeds().add(FunctionFeed.builder()
                    .entity(this)
                    .period(functionFeedPeriod)
                    .poll(FunctionPollConfig.forSensor(FUNCTION_COUNTER)
                            .callable(new Callable<Long>() {
                                @Override public Long call() throws Exception {
                                    Long oldVal = sensors().get(FUNCTION_COUNTER);
                                    return (oldVal == null) ? 1 : oldVal + 1;
                                }
                            }))
                    .build());
        }
        
        if (httpFeedUri != null) {
            httpFeed = feeds().add(HttpFeed.builder()
                    .entity(this)
                    .period(config().get(HTTP_FEED_POLL_PERIOD))
                    .baseUri(httpFeedUri)
                    .poll(new HttpPollConfig<Integer>(HTTP_INT_ATTRIBUTE)
                            .onSuccess(HttpValueFunctions.responseCode()))
                    .poll(new HttpPollConfig<String>(HTTP_STRING_ATTRIBUTE)
                            .onSuccess(HttpValueFunctions.stringContentsFunction()))
                    .build());
        }
    }

    @Override
    protected void disconnectSensors() {
        super.disconnectSensors();
        if (functionFeed != null) functionFeed.stop();
        if (httpFeed != null) httpFeed.stop();
    }
    
    public static class SimulatedVanillaSoftwareProcessSshDriver extends VanillaSoftwareProcessSshDriver {
        public SimulatedVanillaSoftwareProcessSshDriver(SimulatedVanillaSoftwareProcessImpl entity, SshMachineLocation machine) {
            super(entity, machine);
        }
        
        @Override
        public void install() {
            if (Boolean.TRUE.equals(entity.getConfig(EXEC_SSH_ON_START))) {
                super.install();
            } else {
                // no-op
            }
        }
        
        @Override
        public void customize() {
            if (Boolean.TRUE.equals(entity.getConfig(EXEC_SSH_ON_START))) {
                super.customize();
            } else {
                // no-op
            }
        }
        
        @Override
        public void launch() {
            if (Boolean.TRUE.equals(entity.getConfig(EXEC_SSH_ON_START))) {
                super.launch();
            } else {
                // no-op
            }
        }
    }
}
