/*
 * 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.entity.java;

import static java.lang.String.format;

import java.io.File;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

import org.apache.brooklyn.entity.software.base.lifecycle.ScriptHelper;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.file.ArchiveBuilder;
import org.apache.brooklyn.util.core.file.ArchiveUtils;
import org.apache.brooklyn.util.core.task.Tasks;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.net.Urls;
import org.apache.brooklyn.util.os.Os;
import org.apache.brooklyn.util.text.Strings;
import org.apache.brooklyn.util.text.StringEscapes.BashStringEscapes;

import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

/**
 * The SSH implementation of the {@link VanillaJavaAppDriver}.
 */
public class VanillaJavaAppSshDriver extends JavaSoftwareProcessSshDriver implements VanillaJavaAppDriver {

    // FIXME this should be a config, either on the entity or -- probably better -- 
    // an alternative / override timeout on the SshTool for file copy commands
    final static int NUM_RETRIES_FOR_COPYING = 4;
    
    public VanillaJavaAppSshDriver(VanillaJavaAppImpl entity, SshMachineLocation machine) {
        super(entity, machine);
    }

    @Override
    public VanillaJavaAppImpl getEntity() {
        return (VanillaJavaAppImpl) super.getEntity();
    }

    @Override
    protected String getLogFileLocation() {
        return Os.mergePathsUnix(getRunDir(), "console");
    }

    @Override
    public void install() {
        newScript(INSTALLING).execute();
    }

    @Override
    public void customize() {
        newScript(CUSTOMIZING)
                .body.append(format("mkdir -p %s/lib", getRunDir()))
                .failOnNonZeroResultCode()
                .execute();

        SshMachineLocation machine = getMachine();
        VanillaJavaApp entity = getEntity();
        for (String entry : entity.getClasspath()) {
            // If a local folder, then create archive from contents first
            if (Urls.isDirectory(entry)) {
                File jarFile = ArchiveBuilder.jar().addDirContentsAt(new File(entry), "").create();
                entry = jarFile.getAbsolutePath();
            }

            // Determine filename
            String destFile = entry.contains("?") ? entry.substring(0, entry.indexOf('?')) : entry;
            destFile = destFile.substring(destFile.lastIndexOf('/') + 1);

            ArchiveUtils.deploy(MutableMap.<String, Object>of(), entry, machine, getRunDir(), Os.mergePaths(getRunDir(), "lib"), destFile);
        }

        ScriptHelper helper = newScript(CUSTOMIZING+"-classpath")
                .body.append(String.format("ls -1 \"%s\"", Os.mergePaths(getRunDir(), "lib")))
                .noExtraOutput()
                .gatherOutput();
        int result = helper.execute();
        if (result != 0) {
            throw new IllegalStateException("Error listing classpath files: " + helper.getResultStderr());
        }
        String stdout = helper.getResultStdout();

        // Transform stdout into list of files in classpath
        if (Strings.isBlank(stdout)) {
            getEntity().sensors().set(VanillaJavaApp.CLASSPATH_FILES, ImmutableList.of(Os.mergePaths(getRunDir(), "lib")));
        } else {
            // FIXME Cannot handle spaces in paths properly
            Iterable<String> lines = Splitter.on(CharMatcher.BREAKING_WHITESPACE).omitEmptyStrings().trimResults().split(stdout);
            Iterable<String> files = Iterables.transform(lines, new Function<String, String>() {
                        @Override
                        public String apply(@Nullable String input) {
                            return Os.mergePathsUnix(getRunDir(), "lib", input);
                        }
                    });
            getEntity().sensors().set(VanillaJavaApp.CLASSPATH_FILES, ImmutableList.copyOf(files));
        }
    }

    public String getClasspath() {
        @SuppressWarnings("unchecked")
        List<String> files = getEntity().getAttribute(VanillaJavaApp.CLASSPATH_FILES);
        if (files == null || files.isEmpty()) {
            return null;
        } else {
            return Joiner.on(":").join(files);
        }
    }

    @Override
    public void launch() {
        String clazz = getEntity().getMainClass();
        String args = getArgs();

        newScript(MutableMap.of(USE_PID_FILE, true), LAUNCHING)
            .body.append(
                    format("echo \"launching: java $JAVA_OPTS %s %s\"", clazz, args),
                    format("java $JAVA_OPTS -cp \"%s\" %s %s >> %s/console 2>&1 </dev/null &", getClasspath(), clazz, args, getRunDir())
                )
            .execute();
    }

    public String getArgs(){
        List<Object> args = getEntity().getConfig(VanillaJavaApp.ARGS);
        StringBuilder sb = new StringBuilder();
        Iterator<Object> it = args.iterator();
        while (it.hasNext()) {
            Object argO = it.next();
            try {
                String arg = Tasks.resolveValue(argO, String.class, getEntity().getExecutionContext());
                BashStringEscapes.assertValidForDoubleQuotingInBash(arg);
                sb.append(format("\"%s\"",arg));
            } catch (Exception e) {
                throw Exceptions.propagate(e);
            }
            if (it.hasNext()) {
                sb.append(" ");
            }
        }

        return sb.toString();
    }

    @Override
    public boolean isRunning() {
        int result = newScript(MutableMap.of(USE_PID_FILE, true), CHECK_RUNNING).execute();
        return result == 0;
    }

    @Override
    public void stop() {
        newScript(MutableMap.of(USE_PID_FILE, true), STOPPING).execute();
    }

    @Override
    public void kill() {
        newScript(MutableMap.of(USE_PID_FILE, true), KILLING).execute();
    }

    @Override
    protected Map getCustomJavaSystemProperties() {
        return MutableMap.builder()
                .putAll(super.getCustomJavaSystemProperties())
                .putAll(getEntity().getJvmDefines())
                .build();
    }

    @Override
    protected List<String> getCustomJavaConfigOptions() {
        return MutableList.<String>builder()
                .addAll(super.getCustomJavaConfigOptions())
                .addAll(getEntity().getJvmXArgs())
                .build();
    }

    @Override
    public Map<String,String> getShellEnvironment() {
        return MutableMap.<String,String>builder()
                .putAll(super.getShellEnvironment())
                .putIfNotNull("CLASSPATH", getClasspath())
                .build();
    }
}
