/*
 * Copyright 2012 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Eclipse Public License version 1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.jboss.forge.shell.command;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

import javax.enterprise.inject.Instance;
import javax.inject.Inject;

import org.jboss.forge.resources.Resource;
import org.jboss.forge.shell.PromptType;
import org.jboss.forge.shell.Shell;
import org.jboss.forge.shell.ShellMessages;
import org.jboss.forge.shell.command.parser.CommandParser;
import org.jboss.forge.shell.command.parser.CommandParserContext;
import org.jboss.forge.shell.command.parser.CompositeCommandParser;
import org.jboss.forge.shell.command.parser.NamedBooleanOptionParser;
import org.jboss.forge.shell.command.parser.NamedValueOptionParser;
import org.jboss.forge.shell.command.parser.NamedValueVarargsOptionParser;
import org.jboss.forge.shell.command.parser.NullTokenOptionParser;
import org.jboss.forge.shell.command.parser.OrderedValueOptionParser;
import org.jboss.forge.shell.command.parser.OrderedValueVarargsOptionParser;
import org.jboss.forge.shell.exceptions.PluginExecutionException;
import org.jboss.forge.shell.plugins.PipeOut;
import org.jboss.forge.shell.util.Enums;
import org.jboss.forge.shell.util.GeneralUtils;
import org.jboss.forge.shell.util.Packages;
import org.mvel2.util.ParseTools;

/**
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
public class ExecutionParser
{
   private final PluginRegistry registry;
   private final Instance<Execution> executionInstance;
   private final Shell shell;
   private final PromptTypeConverter promptTypeConverter;

   @Inject
   public ExecutionParser(final PluginRegistry registry, final Instance<Execution> executionInstance,
            final Shell shell, final PromptTypeConverter promptTypeConverter)
   {
      this.registry = registry;
      this.executionInstance = executionInstance;
      this.shell = shell;
      this.promptTypeConverter = promptTypeConverter;
   }

   @SuppressWarnings({ "unchecked", "rawtypes" })
   public Execution parse(final Queue<String> tokens, final String pipeIn, final PipeOut pipeOut)
   {
      Execution execution = executionInstance.get();
      // execution.setOriginalStatement(line);
      CommandMetadata command = null;

      if (!tokens.isEmpty())
      {
         String first = tokens.remove();
         execution.setOriginalStatement(first);
         PluginMetadata plugin = registry.getPluginMetadataForScopeAndConstraints(first, shell);

         if (plugin != null)
         {
            if (!tokens.isEmpty())
            {
               String second = tokens.peek();
               command = plugin.getCommand(second, shell);

               if (command != null)
               {
                  if (!command.isDefault())
                  {
                     tokens.remove();
                  }
               }
               else if (plugin.hasDefaultCommand())
               {
                  command = plugin.getDefaultCommand();
               }
            }
            else if (plugin.hasDefaultCommand())
            {
               command = plugin.getDefaultCommand();
            }

            if (command != null)
            {
               if (!command.usableWithResource(shell.getCurrentResource().getClass()))
               {
                  // noinspection unchecked
                  throw new PluginExecutionException(plugin, "command '"
                           + command.getName()
                           + "' is not usable in current scope ["
                           + shell.getCurrentResource().getClass().getSimpleName()
                           + "]"
                           + " -- usable scopes: "
                           + GeneralUtils.elementSetSimpleTypesToString((Set) command.getResourceScopes()));
               }

               execution.setCommand(command);

               // parse parameters and set order / nulls for command invocation

               Object[] parameters = parseParameters(command, tokens, pipeIn, pipeOut);
               execution.setParameterArray(parameters);
            }
            else
            {
               throw new PluginExecutionException(plugin, "Missing command for plugin [" + plugin.getName()
                        + "], available commands: " + plugin.getCommands(shell));
            }
         }
         else
         {
            List<PluginMetadata> pluginMetadata = registry.getPluginMetadata(first);
            if ((pluginMetadata != null) && !pluginMetadata.isEmpty())
            {
               Set<Class<? extends Resource<?>>> aggregate = new HashSet<Class<? extends Resource<?>>>();
               for (PluginMetadata meta : pluginMetadata)
               {
                  Set<Class<? extends Resource<?>>> scopes = meta.getResourceScopes();
                  aggregate.addAll(scopes);
               }

               throw new PluginExecutionException(pluginMetadata.get(0),
                        "Plugin is not usable in current scope or project.");
            }
         }
      }

      return execution;
   }

   @SuppressWarnings({ "rawtypes", "unchecked" })
   private Object[] parseParameters(final CommandMetadata command, final Queue<String> tokens, final String pipeIn,
            final PipeOut pipeOut)
   {
      CommandParser commandParser = new CompositeCommandParser(new NamedBooleanOptionParser(),
               new NamedValueOptionParser(), new NamedValueVarargsOptionParser(), new OrderedValueOptionParser(),
               new OrderedValueVarargsOptionParser(), new NullTokenOptionParser());

      CommandParserContext context = new CommandParserContext();
      Map<OptionMetadata, Object> valueMap = commandParser.parse(command, tokens, context)
               .getValueMap();

      for (String warning : context.getWarnings())
      {
         ShellMessages.info(shell, warning);
      }

      Object[] parameters = new Object[command.getOptions().size()];
      for (OptionMetadata option : command.getOptions())
      {

         PromptType promptType = option.getPromptType();
         String defaultValue = option.getDefaultValue();
         Class<?> optionType = option.getBoxedType();
         String optionDescriptor = option.getOptionDescriptor() + ": ";

         Object value;
         if (option.isPipeOut())
         {
            value = pipeOut;
         }
         else if (option.isPipeIn())
         {
            value = pipeIn;

            if (pipeIn != null)
            {
               if (InputStream.class.isAssignableFrom(option.getBoxedType()))
               {
                  value = new InputStream()
                  {
                     int cursor = 0;
                     int len = pipeIn.length();

                     @Override
                     public int read() throws IOException
                     {
                        return cursor != len ? pipeIn.charAt(cursor++) : -1;
                     }
                  };
               }
            }

         }
         else
         {
            value = valueMap.get(option);
         }

         if (!option.isPipeOut() && !option.isPipeIn())
         {
            // TODO Is this really where we want to do PromptType conversion?
            value = doPromptTypeConversions(value, promptType);

            if ((value != null) && option.getBoxedType().isEnum() && !Enums.hasValue(option.getType(), value))
            {
               ShellMessages.info(shell, "Could not parse [" + value + "]... please try again...");
               if (!option.hasCustomCompleter())
               {
                  value = shell.promptEnum(optionDescriptor, (Class<Enum>) option.getType());
               }
               else
               {
                  value = shell.promptCompleter(optionDescriptor, option.getCompleterType());
               }
            }
            else if (((value != null) && (promptType != null)) && !promptType.matches(value.toString()))
            {
               // make sure the current option value is OK
               ShellMessages.info(shell, "Could not parse [" + value + "]... please try again...");
               if (promptType.equals(PromptType.JAVA_PACKAGE))
               {
                  String defaultPackage = value == null ? "" : Packages.toValidPackageName((String) value);
                  value = shell.promptCommon(optionDescriptor, promptType, defaultPackage);
               }
               else
               {
                  value = shell.promptCommon(optionDescriptor, promptType);
               }
            }
            else if (option.isRequired() && (value == null) && (!option.hasDefaultValue()))
            {
               while (value == null)
               {
                  if (option.isEnum())
                  {
                     value = shell.promptEnum(optionDescriptor, (Class<Enum>) option.getType());
                  }
                  else if (isBooleanOption(option))
                  {
                     value = shell.promptBoolean(optionDescriptor);
                  }
                  else if (isFileOption(optionType))
                  {
                     value = shell.promptFile(optionDescriptor);
                  }
                  else if ((promptType != null) && !PromptType.ANY.equals(promptType))
                  {
                     // make sure an omitted required option value is OK
                     value = shell.promptCommon(optionDescriptor, promptType);
                  }
                  else
                  {
                     value = shell.prompt(optionDescriptor);
                  }

                  if (String.valueOf(value).trim().length() == 0)
                  {
                     ShellMessages.info(shell, "The option is required to execute this command.");
                     value = null;
                  }
               }
            }
            else if ((value == null) && (option.hasDefaultValue()))
            {
               value = defaultValue;
            }
         }

         parameters[option.getIndex()] = value;
         // Default values seem to be ignored for Enums
      }

      return parameters;
   }

   private Object doPromptTypeConversions(Object value, final PromptType promptType)
   {
      if ((value != null) && value.getClass().isArray())
      {
         Object[] values = (Object[]) value;
         for (int i = 0; i < values.length; i++)
         {
            values[i] = promptTypeConverter.convert(promptType, (String) values[i]);
         }

         value = values;
      }
      else
      {
         value = promptTypeConverter.convert(promptType, (String) value);
      }
      return value;
   }

   public boolean isFileOption(final Class<?> optionType)
   {
      return File.class.isAssignableFrom(optionType);
   }

   private static boolean isBooleanOption(final OptionMetadata option)
   {
      return ParseTools.unboxPrimitive(option.getType()) == boolean.class;
   }
}
