/*
 * Copyright 2010 the original author or authors.
 *
 * Licensed 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.gradle.foundation.ipc.gradle;

import org.apache.commons.io.IOUtils;
import org.gradle.initialization.DefaultCommandLineConverter;
import org.gradle.StartParameter;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.foundation.CommandLineAssistant;
import org.gradle.foundation.ipc.basic.MessageObject;
import org.gradle.foundation.ipc.basic.ProcessLauncherServer;
import org.gradle.foundation.ipc.basic.ExecutionInfo;
import org.gradle.foundation.ipc.basic.ClientProcess;
import org.gradle.util.OperatingSystem;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/**
 * This defines the basic behavior of all gradle protocols for interprocess communication. It manages handshaking,
 * detecting if the client executed prematurely, as well as executing alternate external processes. All you need to do
 * is extend this, implement the abstract functions, and make sure you call setHasReceivedBuildCompleteNotification()
 * when whatever you were doing is complete (so we know any exiting is not premature).
 *
 * @author mhunsicker
  */
public abstract class AbstractGradleServerProtocol implements ProcessLauncherServer.Protocol
{
   private static final String INIT_SCRIPT_EXTENSION = ".gradle";

   private final Logger logger = Logging.getLogger( AbstractGradleServerProtocol.class );

   protected ProcessLauncherServer server;
   private boolean continueConnection;
   private boolean waitingOnHandshakeCompletion;
   private boolean hasCompletedConnection;

   private boolean hasReceivedBuildCompleteNotification;

   private File currentDirectory;
   private File gradleHomeDirectory;

   private File customGradleExecutor;
   private String commandLine;
   private LogLevel logLevel;

   //all of this is just so we can get gradle to kill itself when we cancel
   private int killGradleServerPort;
   private KillGradleClientProtocol killGradleClientProcotol;
   private ClientProcess killGradleClient;

   protected MessageObject lastMessageReceived; //just for debugging purposes

   /**
     * @return true if we should keep the connection alive. False if we should stop communicaiton.
   */
   public boolean continueConnection()
   {
      return continueConnection;
   }

   private StartParameter.ShowStacktrace stackTraceLevel;

   public AbstractGradleServerProtocol( File currentDirectory, File gradleHomeDirectory, File customGradleExecutor, String fullCommandLine, LogLevel logLevel, StartParameter.ShowStacktrace stackTraceLevel)
   {
      this.currentDirectory = currentDirectory;
      this.gradleHomeDirectory = gradleHomeDirectory;
      this.customGradleExecutor = customGradleExecutor;
      this.commandLine = fullCommandLine;
      this.logLevel = logLevel;
      this.stackTraceLevel = stackTraceLevel;
   }

   /**
     * Notification that the connection was accepted by the client.
   */
   public void connectionAccepted()
   {
      //let's make sure we're talking to the right client with some tiny handshaking.
      server.sendMessage( ProtocolConstants.HANDSHAKE_TYPE, ProtocolConstants.HANDSHAKE_SERVER );
      continueConnection = true;
      waitingOnHandshakeCompletion = true;
   }

   /**
     * Gives your protocol a chance to store this server so it can access its functions.
   */
   public void initialize( ProcessLauncherServer server ) { this.server = server; }

   /**
     * Call this to stop communication
   */
   protected void closeConnection()
   {
      this.continueConnection = false;
   }

   /**
     * Notification that a message has been received. If we just connected, we'll do a quick handshake to verify the
     * client, then we just pass the rest on our our output panel.
     *
     * @param message the message that was received.
   */
   public void messageReceived( MessageObject message )
   {
      lastMessageReceived = message;
      if( waitingOnHandshakeCompletion )  //are we still handshaking?
      {
         if( ProtocolConstants.HANDSHAKE_CLIENT.equalsIgnoreCase( message.getMessage() ) )
         {
            waitingOnHandshakeCompletion = false;  //we've received what we expected
            hasCompletedConnection = true;         //and we're now connected
            if( message.getData() != null )
            {
               killGradleServerPort = (Integer) message.getData();
               killGradleClientProcotol = new KillGradleClientProtocol();
               killGradleClient = new ClientProcess( killGradleClientProcotol );
               killGradleClient.start( killGradleServerPort );
               handShakeCompleted();
            }
            else
            {
               addStatus( "Invalid handshaking. Missing port number. Stopping connection" );
               server.sendMessage( "?", "Invalid client handshake protocol!" );
               closeConnection();
            }
         }
         else
         {
            addStatus( "Invalid handshaking. Stopping connection" );
            server.sendMessage( "?", "Invalid client handshake protocol!" );
            closeConnection();
         }
      }
      else  //otherwise, its just a normal message, the protocol should handle it.
      {
         try
         {
            handleMessageReceived( message );
         }
         catch( Throwable e )
         {
            logger.error( "Problem while handing message :\n" + message, e );
         }
      }
   }

   /**
     * This provides you with a chance to do something when the handshaking is complete
    */
   protected void handShakeCompleted()
   {

   }

   /**
     * Notification that a message was received that we didn't process. Implement this to handle the specifics of your
     * protocol. Basically, the base class handles the handshake. The rest of the conversation is up to you.
     *
     * @param message the message we received.
     * @return true if we handled the message, false if not. If we don't know it, we won't return an acknowlegement.
   */
   protected abstract boolean handleMessageReceived( MessageObject message );

   /**
     * Call this to mark the build as completed (whether successfully or not). This is used to determine if the client
     * has exited prematurely which indicates a problem.
   */
   public void setHasReceivedBuildCompleteNotification() { this.hasReceivedBuildCompleteNotification = true; }

   /**
     * Notification of any status that might be helpful to the user.
     *
     * @param status a status message
   */
   protected abstract void addStatus(String status );

            public class MyExecutionInfo implements ExecutionInfo {
                public String[] commandLineArguments;
                public File workingDirectory;
                public HashMap<String, String> environmentVariables = new HashMap<String, String>();
                public File initStriptPath;

                public String[] getCommandLineArguments() {
                    return commandLineArguments;
                }

                public File getWorkingDirectory() {
                    return workingDirectory;
                }

                public HashMap<String, String> getEnvironmentVariables() {
                    return environmentVariables;
                }

                public void setCommandLineArguments(String[] commandLineArguments) {
                    this.commandLineArguments = commandLineArguments;
                }

                public void setWorkingDirectory(File workingDirectory) {
                    this.workingDirectory = workingDirectory;
                }

                public void addEnvironmentVariable(String name, String value) {
                    this.environmentVariables.put(name, value);
                }

                public void processExecutionComplete() {
                    if( initStriptPath != null ) {
                        initStriptPath.delete();
                    }
                }
            }


   /**
     * Fill in the ExecutionInfo object with information needed to execute the other process.
         * @param serverPort the port the server is listening on. The client should send messages here
         * @return an executionInfo object containing information about what we execute.
   */
   public ExecutionInfo getExecutionInfo( int serverPort )
   {
      MyExecutionInfo executionInfo = new MyExecutionInfo();

      //set some environment variables that need to be passed to the script.
      executionInfo.addEnvironmentVariable( "GRADLE_HOME", getGradleHomeDirectory().getAbsolutePath() );
      executionInfo.addEnvironmentVariable( "JAVA_HOME", System.getProperty( "java.home" ) );

      executionInfo.setWorkingDirectory( currentDirectory );

      List<String> executionCommandLine = new ArrayList<String>();

      //put the file to execute on the command line
      File gradleExecutableFile = getGradleExecutableFile();
      if( gradleExecutableFile == null ) {
          throw new RuntimeException( "Gradle executable not specified" );
      }
      if( !gradleExecutableFile.exists() ) {
          throw new RuntimeException( "Missing gradle executable. Expected it at: " + gradleExecutableFile );
      }
       executionCommandLine.add( gradleExecutableFile.getAbsolutePath() );

      //add the port number we're listenening on
      executionCommandLine.add( "-D" + ProtocolConstants.PORT_NUMBER_SYSTEM_PROPERTY + "=" + Integer.toString( serverPort ) );

      CommandLineAssistant commandLineAssistant = new CommandLineAssistant();

      //add whatever the user ran
      String[] individualCommandLineArguments = commandLineAssistant.breakUpCommandLine( commandLine );
      executionCommandLine.addAll( Arrays.asList( individualCommandLineArguments ) );

      File initStriptPath = getInitScriptFile();
      if( initStriptPath != null )
      {
          executionCommandLine.add( "-" + DefaultCommandLineConverter.INIT_SCRIPT );
          executionCommandLine.add( initStriptPath.getAbsolutePath() );
          executionInfo.initStriptPath = initStriptPath;
      }

      //add the log level if its not present
        if (!commandLineAssistant.hasLogLevelDefined(individualCommandLineArguments)) {
            String logLevelText = commandLineAssistant.getLoggingCommandLineConverter().getLogLevelCommandLine(logLevel);
            if (logLevelText != null && !"".equals(logLevelText)) {
            executionCommandLine.add( '-' + logLevelText );
      }
        }

      //add the stack trace level if its not present
        if (!commandLineAssistant.hasShowStacktraceDefined(individualCommandLineArguments)) {
            String stackTraceLevelText = commandLineAssistant.getCommandLineConverter().getShowStacktraceCommandLine(stackTraceLevel);
            if (stackTraceLevelText != null) {
            executionCommandLine.add( '-' + stackTraceLevelText );
      }
        }

      executionInfo.setCommandLineArguments( executionCommandLine.toArray( new String[ executionCommandLine.size() ] ) );
      return executionInfo;
   }

   /**
     * @return the file that should be used to execute gradle. If we've been given a custom file, we use that, otherwise,
     *         we use the batch or shell script inside the gradle home's bin directory.
   */
    protected File getGradleExecutableFile() {
        if (customGradleExecutor != null) {
         return customGradleExecutor;
        }

      return new File( gradleHomeDirectory, "bin" + File.separator + getDefaultGradleExecutableName() );
   }

   /**
     * This determines what we're going to execute. Its different based on the OS.
     *
     * @return whatever we're going to execute.
   */
   private String getDefaultGradleExecutableName()
   {
       return OperatingSystem.current().getScriptName("gradle");
   }

   /**
     * Notification that the client has stopped all communications.
   */
   public void clientCommunicationStopped()
   {
      //we don't really care
   }

   /**
     * Notification that the client has shutdown. Note: this can occur before communciations has ever started. You SHOULD
     * get this notification before receiving serverExited, even if the client fails to launch or locks up.
     *
     * @param returnCode the return code of the client application
     * @param output the standard error and standard output of the client application
    */
   public void clientExited( int returnCode, String output )
   {
      server.requestShutdown();

      boolean wasPremature = false;
      String message;

      if( !hasCompletedConnection ) //if we never connected, report it
      {
         message = "Failed to connect to gradle process for command '" + commandLine +"'\n" + output;
         wasPremature = true;
      }
      else
         if( !hasReceivedBuildCompleteNotification  )  //this may happen if the client doesn't execute properly or it was killed/canceled. This is just so we don't lose our output (which may yeild clues to the problem).
         {
            message = output;
            wasPremature = true;
         }
         else
         {
            message = output;
         }

      reportClientExit( wasPremature, returnCode, message );
   }

   /**
     * This is called if the client exits prematurely. That is, we never connected to it or it didn't finish. This can
     * happen because of setup issues or errors that occur in gradle.
     *
     * @param returnCode the return code of the application
     * @param message Whatever information we can gleen about what went wrong.
    */
   protected abstract void reportClientExit( boolean wasPremature, int returnCode, String output );

   /**
     * This is called before we execute a command. Here, return an init script for this protocol. An init script is a
     * gradle script that gets run before the other scripts are processed. This is useful here for initiating the gradle
     * client that talks to the server.
     *
     * @return The path to an init script. Null if you have no init script.
   */
   public abstract File getInitScriptFile();

   /**
     * If you do have an init script that's a resource, this will extract it based on the name and write it to a
     * temporary file and delete it on exit.
     *
     * @param resourceClass the class associated with the resource
     * @param resourceName the name (minus extension or '.') of the resource
    */
   protected File extractInitScriptFile( Class resourceClass, String resourceName )
   {
       File file = null;
       try
       {
           file = File.createTempFile( resourceName, INIT_SCRIPT_EXTENSION );
       }
       catch (IOException e)
       {
           logger.error( "Creating init script file temp file", e );
           return null;
       }
       file.deleteOnExit();

        if (extractResourceAsFile(resourceClass, resourceName + INIT_SCRIPT_EXTENSION, file)) {
         return file;
        }

      logger.error( "Internal error! Failed to extract init script for executing commands!" );

      return null;
   }

   /**
     * This extracts the given class' resource to the specified file if it doesn't already exist.
     *
     * @param resourceClass the class associated with the resource
     * @param name the resource's name
     * @param file where to put the resource
     * @return true if successful, false if not.
   */
   public boolean extractResourceAsFile( Class resourceClass, String name, File file )
   {
      InputStream stream = resourceClass.getResourceAsStream( name );
        if (stream == null) {
         return false;
        }

      byte[] bytes = new byte[0];
      try
      {
         bytes = IOUtils.toByteArray( stream );
      }
      catch( IOException e )
      {
         logger.error( "Extracting resource as file", e );
         return false;
      }

      FileOutputStream fileOutputStream = null;
      try
      {
         fileOutputStream = new FileOutputStream( file );
          try {
              IOUtils.write( bytes, fileOutputStream );
          } finally {
              fileOutputStream.close();
          }
          return true;
      }
      catch( IOException e )
      {
         logger.error( "Extracting resource as file (writing bytes)", e );
         return false;
      }
      finally
      {
         IOUtils.closeQuietly( fileOutputStream );
      }
   }

   protected File getGradleHomeDirectory() { return gradleHomeDirectory; }

   /**
     * Notification that a read failure occurred. This really only exists for debugging purposes when things go wrong.
    */
   public void readFailureOccurred()
   {
      logger.debug( "Last message received: " + lastMessageReceived );
   }

   public void aboutToKillProcess()
   {
      killGradle();
   }

    public void killGradle() {
        if (killGradleClientProcotol != null) {
         killGradleClientProcotol.sendKillMessage();
         }
   }
}
