/*
 * Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
 *
 * 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.jboss.errai.bus.client.api.base;

import org.jboss.errai.common.client.api.ErrorCallback;
import org.jboss.errai.bus.client.api.messaging.Message;
import org.jboss.errai.bus.client.api.messaging.MessageBus;
import org.jboss.errai.bus.client.api.messaging.RequestDispatcher;
import org.jboss.errai.bus.client.api.RoutingFlag;
import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.common.client.api.ResourceProvider;
import org.jboss.errai.common.client.protocols.MessageParts;
import org.jboss.errai.common.client.types.TypeHandlerFactory;

import java.util.HashMap;
import java.util.Map;

/**
 * The default implementation of the Message interface. The usual way to create a CommandMessage is through the
 * {@link MessageBuilder} API.
 *
 * @see ConversationMessage
 * @see MessageBuilder
 * @author Mike Brock
 * @author Max Barkley <mbarkley@redhat.com>
 */
@SuppressWarnings({"UnusedDeclarations"})
public class CommandMessage implements Message {
  transient final Map<String, ResourceProvider<?>> providedParts;
  final Map<String, Object> parts;
  transient Map<String, Object> resources;
  ErrorCallback errorsCall;
  int routingFlags;

  /**
   * Creates a new CommandMessage with no parts and no provided parts.
   *
   * @return a new instance of CommandMessage.
   */
  public static CommandMessage create() {
    return new CommandMessage();
  }

  /**
   * Creates a new CommandMessage with the given parts and no provided parts.
   * <p>
   * Note that {@link MessageBuilder} is the preferred API for creating Message
   * instances.
   *
   * @param parts
   *          The parts that this message should have initially. This map is
   *          taken as-is. No copy is made. Changes to the provided map will be
   *          reflected in this message, and additional parts given to this
   *          message will appear in the provided map.
   */
  public static CommandMessage createWithParts(final Map<String, Object> parts) {
    return new CommandMessage(Assert.notNull(parts));
  }

  @SuppressWarnings("unchecked")
  public static CommandMessage createWithPartsFromRawMap(final Map parts) {
    return new CommandMessage(parts);
  }

  public static CommandMessage createWithParts(final Map<String, Object> parts, final int flags) {
    return new CommandMessage(Assert.notNull(parts), flags);
  }


  /**
   * Creates a new CommandMessage with the given parts and provided parts.
   * <p>
   * Note that {@link MessageBuilder} is the preferred API for creating Message
   * instances.
   *
   * @param parts
   *          The parts that this message should have initially. This map is
   *          taken as-is. No copy is made. Changes to the provided map will be
   *          reflected in this message, and additional parts given to this
   *          message will appear in the provided map.
   * @param provided
   *          The provided parts that this message should have initially. This
   *          map is taken as-is. No copy is made. Changes to the provided map
   *          will be reflected in this message, and additional parts given to
   *          this message will appear in the provided map.
   */
  public static CommandMessage createWithParts(final Map<String, Object> parts, final Map<String, ResourceProvider<?>> provided) {
    return new CommandMessage(parts, provided);
  }

  CommandMessage() {
    this.parts = new HashMap<String, Object>();
    this.providedParts = new HashMap<String, ResourceProvider<?>>(5);
  }

  private CommandMessage(final Map<String, Object> parts) {
    this.parts = parts;
    this.providedParts = new HashMap<String, ResourceProvider<?>>(0);
  }

  public CommandMessage(final Map<String, Object> parts, final int routingFlags) {
    this.parts = parts;
    this.routingFlags = routingFlags;
    this.providedParts = new HashMap<String, ResourceProvider<?>>(5);
  }

  private CommandMessage(final Map<String, Object> parts, final Map<String, ResourceProvider<?>> providers) {
    this.parts = parts;
    this.providedParts = providers;
  }

  @Override
  public String getCommandType() {
    return (String) parts.get(MessageParts.CommandType.name());
  }

  @Override
  public String getSubject() {
    return String.valueOf(parts.get(MessageParts.ToSubject.name()));
  }

  @Override
  public Message toSubject(final String subject) {
    parts.put(MessageParts.ToSubject.name(), subject);
    return this;
  }

  @Override
  public Message command(final Enum<?> type) {
    parts.put(MessageParts.CommandType.name(), type.name());
    return this;
  }

  @Override
  public Message command(final String type) {
    parts.put(MessageParts.CommandType.name(), type);
    return this;
  }

  @Override
  public Message set(final Enum<?> part, final Object value) {
    return set(part.name(), value);
  }

  @Override
  public Message set(final String part, final Object value) {
    parts.put(part, value);
    return this;
  }

  @Override
  public Message setProvidedPart(final String part, final ResourceProvider<?> provider) {
    providedParts.put(part, provider);
    return this;
  }

  @Override
  public Message setProvidedPart(final Enum<?> part, final ResourceProvider<?> provider) {
    return setProvidedPart(part.name(), provider);
  }

  @Override
  public void remove(final String part) {
    parts.remove(part);
  }

  @Override
  public void remove(final Enum<?> part) {
    parts.remove(part.name());
  }

  @Override
  public Message copy(final Enum<?> part, final Message message) {
    set(part, message.get(Object.class, part));
    return this;
  }

  @Override
  public Message copy(final String part, final Message message) {
    set(part, message.get(Object.class, part));
    return this;
  }

  @Override
  public Message setFlag(final RoutingFlag flag) {
    routingFlags |= flag.flag();
    return this;
  }

  @Override
  public void unsetFlag(final RoutingFlag flag) {
    if ((routingFlags & flag.flag()) != 0) {
      routingFlags ^= flag.flag();
    }
  }

  @Override
  public boolean isFlagSet(final RoutingFlag flag) {
    return (routingFlags & flag.flag()) != 0;
  }

  @Override
  public <T> T getValue(Class<T> type) {
    return get(type, MessageParts.Value);
  }

  @Override
  @SuppressWarnings({ "UnusedDeclaration" })
  public <T> T get(final Class<T> type, final Enum<?> part) {
    return get(type, part.toString());
  }

  @Override
  @SuppressWarnings({ "UnusedDeclaration" })
  public <T> T get(final Class<T> type, final String part) {
    final Object value = parts.get(part);
    return value == null ? null : TypeHandlerFactory.convert(value.getClass(), type, value);
  }

  @Override
  public boolean hasPart(final Enum<?> part) {
    return hasPart(part.name());
  }

  @Override
  public boolean hasPart(final String part) {
    return parts.containsKey(part);
  }

  @Override
  public Map<String, Object> getParts() {
    return parts;
  }

  @Override
  public Map<String, ResourceProvider<?>> getProvidedParts() {
    return providedParts;
  }

  @Override
  public Message setParts(final Map<String, Object> parts) {
    parts.clear();
    parts.putAll(parts);
    return this;
  }

  @Override
  public Message addAllParts(final Map<String, Object> parts) {
    this.parts.putAll(parts);
    return this;
  }

  @Override
  public Message addAllProvidedParts(final Map<String, ResourceProvider<?>> parts) {
    this.providedParts.putAll(parts);
    return this;
  }

  @Override
  public Message setResource(final String key, final Object res) {
    if (this.resources == null)
      this.resources = new HashMap<String, Object>();
    this.resources.put(key, res);
    return this;
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T> T getResource(final Class<T> type, final String key) {
    return (T) (this.resources == null ? null : this.resources.get(key));
  }

  @Override
  public Message copyResource(final String key, final Message copyFrom) {
    if (!copyFrom.hasResource(key)) {
      throw new RuntimeException("Cannot copy resource '" + key + "': no such resource.");
    }
    setResource(key, copyFrom.getResource(Object.class, key));
    return this;
  }

  @Override
  public Message errorsCall(final ErrorCallback callback) {
    if (this.errorsCall != null) {
      throw new RuntimeException("An ErrorCallback is already registered");
    }
    this.errorsCall = callback;
    return this;
  }

  @Override
  public ErrorCallback getErrorCallback() {
    return errorsCall;
  }

  @Override
  public boolean hasResource(final String key) {
    return this.resources != null && this.resources.containsKey(key);
  }

  @Override
  public void addResources(final Map<String, ?> resources) {
    if (this.resources == null) {
      this.resources = new HashMap<String, Object>(resources);
    }
    else {
      this.resources.putAll(resources);
    }
  }

  @Override
  public void commit() {
    if (!providedParts.isEmpty()) {
      for (final Map.Entry<String, ResourceProvider<?>> entry : providedParts.entrySet())
        set(entry.getKey(), entry.getValue().get());
    }
  }

  @Override
  public boolean isCommited() {
    return isFlagSet(RoutingFlag.Committed);
  }

  @Override
  public void sendNowWith(final MessageBus viaThis) {
    if (ConversationHelper.hasConversationCallback(this)) {
      ConversationHelper.createConversationService(viaThis, this);
    }
    viaThis.send(this);
  }

  @Override
  public void sendNowWith(final RequestDispatcher viaThis) {
    try {
      viaThis.dispatch(this);
    }
    catch (Exception e) {
      throw new MessageDeliveryFailure("could not deliver message: " + e.getMessage(), e);
    }
  }

  @Override
  public String toString() {
    return buildDescription();
  }

  private String buildDescription() {
    final StringBuilder append = new StringBuilder();
    boolean f = false;
    for (final Map.Entry<String, Object> entry : parts.entrySet()) {
      if (f)
        append.append(", ");
      append.append(entry.getKey()).append("=").append(String.valueOf(entry.getValue()));
      f = true;
    }
    return append.toString();
  }
}
