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

import static com.google.common.base.Objects.equal;
import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.concat;
import static java.lang.String.format;
import static org.jclouds.scriptbuilder.ScriptBuilder.call;
import static org.jclouds.scriptbuilder.ScriptBuilder.findPid;
import static org.jclouds.scriptbuilder.ScriptBuilder.forget;
import static org.jclouds.scriptbuilder.domain.Statements.createRunScript;
import static org.jclouds.scriptbuilder.domain.Statements.interpret;
import static org.jclouds.scriptbuilder.domain.Statements.kill;
import static org.jclouds.scriptbuilder.domain.Statements.newStatementList;
import static org.jclouds.scriptbuilder.domain.Statements.switchArg;

import java.util.Map;

import org.jclouds.scriptbuilder.domain.AcceptsStatementVisitor;
import org.jclouds.scriptbuilder.domain.CreateRunScript;
import org.jclouds.scriptbuilder.domain.OsFamily;
import org.jclouds.scriptbuilder.domain.Statement;
import org.jclouds.scriptbuilder.domain.StatementList;
import org.jclouds.scriptbuilder.domain.StatementVisitor;

import com.google.common.base.Objects;
import com.google.common.collect.ForwardingObject;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

/**
 * Creates an init script file
 */
public class InitScript extends ForwardingObject implements Statement, AcceptsStatementVisitor {

   public static Builder builder() {
      return new Builder();
   }

   public static class Builder {
      protected String instanceName;
      protected String instanceHome = "{tmp}{fs}{varl}INSTANCE_NAME{varr}";
      protected String logDir = "{varl}INSTANCE_HOME{varr}";
      protected Map<String, String> exports = ImmutableMap.of();
      protected StatementList init = new StatementList();
      protected StatementList run = new StatementList();

      /**
       * @see InitScript#getInstanceName()
       */
      public Builder name(String instanceName) {
         this.instanceName = checkNotNull(instanceName, "INSTANCE_NAME");
         return this;
      }

      /**
       * @see InitScript#getInstanceHome()
       */
      public Builder home(String instanceHome) {
         this.instanceHome = checkNotNull(instanceHome, "INSTANCE_HOME");
         return this;
      }

      /**
       * @see InitScript#getLogDir()
       */
      public Builder logDir(String logDir) {
         this.logDir = checkNotNull(logDir, "LOG_DIR");
         return this;
      }

      /**
       * @param exports keys are the variables to export in UPPER_UNDERSCORE case format
       * @see InitScript#getExportedVariables()
       */
      public Builder exportVariables(Map<String, String> exports) {
         this.exports = ImmutableMap.copyOf(checkNotNull(exports, "exports"));
         return this;
      }

      /**
       * @see InitScript#getRun()
       */
      public Builder run(Statement run) {
         this.run = new StatementList(checkNotNull(run, "run"));
         return this;
      }

      /**
       * @see InitScript#getRun()
       */
      public Builder run(Statement... run) {
         this.run = new StatementList(checkNotNull(run, "run"));
         return this;
      }

      /**
       * @see InitScript#getRun()
       */
      public Builder run(Iterable<Statement> run) {
         this.run = new StatementList(checkNotNull(run, "run"));
         return this;
      }

      /**
       * @see InitScript#getRun()
       */
      public Builder run(StatementList run) {
         this.run = checkNotNull(run, "run");
         return this;
      }

      /**
       * @see InitScript#getInit()
       */
      public Builder init(Statement init) {
         this.init = new StatementList(checkNotNull(init, "init"));
         return this;
      }

      /**
       * @see InitScript#getInit()
       */
      public Builder init(Statement... init) {
         this.init = new StatementList(checkNotNull(init, "init"));
         return this;
      }

      /**
       * @see InitScript#getInit()
       */
      public Builder init(Iterable<Statement> init) {
         this.init = new StatementList(checkNotNull(init, "init"));
         return this;
      }

      /**
       * @see InitScript#getInit()
       */
      public Builder init(StatementList init) {
         this.init = checkNotNull(init, "init");
         return this;
      }

      public InitScript build() {
         return new InitScript(instanceName, instanceHome, logDir, exports, init, run);
      }

   }

   protected final String instanceName;
   protected final String instanceHome;
   protected final String logDir;
   protected final Map<String, String> exports;
   protected final StatementList init;
   protected final StatementList run;
   protected final ScriptBuilder delegate;

   /**
    * @param exports keys are the variables to export in UPPER_UNDERSCORE case format
    */
   protected InitScript(String instanceName, String instanceHome, String logDir, Map<String, String> exports,
         StatementList init, StatementList run) {
      this.instanceName = checkNotNull(instanceName, "INSTANCE_NAME");
      this.instanceHome = checkNotNull(instanceHome, "INSTANCE_HOME");
      this.logDir = checkNotNull(logDir, "LOG_DIR");
      this.exports = ImmutableMap.<String, String> copyOf(checkNotNull(exports, "exports"));
      this.init = checkNotNull(init, "init");
      this.run = checkNotNull(run, "run");
      checkArgument(!run.delegate().isEmpty(), "you must specify at least one statement to run");
      this.delegate = makeInitScriptStatement(instanceName, instanceHome, logDir, exports, init, run);
   }

   /**
    *
    * @param exports keys are the variables to export in UPPER_UNDERSCORE case format
    */
   public static ScriptBuilder makeInitScriptStatement(String instanceName, String instanceHome, String logDir,
         Map<String, String> exports, StatementList init, StatementList run) {
      Map<String, String> defaultExports = ImmutableMap.of("INSTANCE_NAME", instanceName, "INSTANCE_HOME", instanceHome,
            "LOG_DIR", logDir);
      String exitStatusFile = format("%s/rc", logDir);
      run = new StatementList(ImmutableList.<Statement> builder().add(interpret("rm -f " + exitStatusFile))
            .add(interpret(format("trap 'echo $?>%s' 0 1 2 3 15", exitStatusFile))).addAll(run.delegate()).build());

      CreateRunScript createRunScript = createRunScript(instanceName,
            concat(exports.keySet(), defaultExports.keySet()), "{varl}INSTANCE_HOME{varr}", run);

      return new ScriptBuilder()
            .addEnvironmentVariableScope("default", defaultExports)
            .addEnvironmentVariableScope(instanceName, exports)
            .addStatement(
                  switchArg(
                        1,
                        new ImmutableMap.Builder<String, Statement>()
                              .put("init", newStatementList(call("default"), call(instanceName), init, createRunScript))
                              .put("status",
                                    newStatementList(call("default"), findPid("{varl}INSTANCE_NAME{varr}"),
                                          interpret("echo {varl}FOUND_PID{varr}{lf}")))
                              .put("stop",
                                    newStatementList(call("default"), findPid("{varl}INSTANCE_NAME{varr}"), kill()))
                              .put("start",
                                    newStatementList(
                                          call("default"),
                                          forget("{varl}INSTANCE_NAME{varr}",
                                                "{varl}INSTANCE_HOME{varr}{fs}{varl}INSTANCE_NAME{varr}.{sh}",
                                                "{varl}LOG_DIR{varr}")))
                              .put("stdout",
                                    newStatementList(call("default"),
                                          interpret("cat {varl}LOG_DIR{varr}{fs}stdout.log{lf}")))
                              .put("stderr",
                                    newStatementList(call("default"),
                                          interpret("cat {varl}LOG_DIR{varr}{fs}stderr.log{lf}")))
                              .put("exitstatus",
                                    newStatementList(call("default"),
                                          interpret("[ -f $LOG_DIR/rc ] && cat $LOG_DIR/rc")))
                              .put("tail",
                                    newStatementList(call("default"),
                                          interpret("tail {varl}LOG_DIR{varr}{fs}stdout.log{lf}")))
                              .put("tailerr",
                                    newStatementList(call("default"),
                                          interpret("tail {varl}LOG_DIR{varr}{fs}stderr.log{lf}")))
                              .put("run",
                                    newStatementList(call("default"),
                                          interpret("{varl}INSTANCE_HOME{varr}{fs}{varl}INSTANCE_NAME{varr}.{sh}{lf}")))
                              .build()));
   }

   /**
    *
    * @return what will be bound to the INSTANCE_NAME variable, and uniquely
    *         identifies the process
    */
   public String getInstanceName() {
      return instanceName;
   }

   /**
    * default {@code /tmp/$INSTANCE_NAME}
    * <br/>
    * <h3>note</h3> The parent directory
    * should be set with unix sticky-bit or otherwise made available to all
    * users. Otherwise, new instances by other users may fail due to not being
    * able to create a directory.  This is why the default is set to {@code /tmp}
    *
    * @return what will be bound to the INSTANCE_HOME variable, and represents
    *         the working directory of the instance
    */
   public String getInstanceHome() {
      return instanceHome;
   }

   /**
    * default {@code $INSTANCE_HOME}
    *
    * @return what will be bound to the LOG_DIR variable, and represents where
    *         stdout and stderr.logs are written.
    */
   public String getLogDir() {
      return logDir;
   }

   /**
    *
    * @return statements that will be executed upon the init command
    */
   public StatementList getInitStatement() {
      return init;
   }

   /**
    *
    * @return statements that will be executed upon the run or start commands
    */
   public StatementList getRunStatement() {
      return init;
   }

   @Override
   public int hashCode() {
      return Objects.hashCode(instanceName);
   }

   @Override
   public boolean equals(Object obj) {
      if (obj == null)
         return false;
      if (!(obj instanceof InitScript)) {
         return false;
      }
      InitScript that = (InitScript) obj;
      return equal(this.instanceName, that.instanceName);
   }

   @Override
   public String toString() {
      return toStringHelper(this).add("INSTANCE_NAME", instanceName).toString();
   }

   @Override
   public void accept(StatementVisitor visitor) {
      delegate().accept(visitor);
   }

   @Override
   public Iterable<String> functionDependencies(OsFamily family) {
      return delegate().functionDependencies(family);
   }

   @Override
   public String render(OsFamily family) {
      return delegate().render(family);
   }

   @Override
   protected ScriptBuilder delegate() {
      return delegate;
   }

}
