package games.strategy.engine.message.unifiedmessenger;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;

import games.strategy.debug.ClientLogger;
import games.strategy.engine.message.MessageContext;
import games.strategy.engine.message.RemoteMethodCall;
import games.strategy.engine.message.RemoteMethodCallResults;
import games.strategy.net.INode;

/**
 * This is where the methods finally get called.
 * An endpoint contains the implementors for a given name that are local to this
 * node.
 * You can invoke the method and get the results for all the implementors.
 */
class EndPoint {
  // the next number we are going to give
  private final AtomicLong m_nextGivenNumber = new AtomicLong();
  // the next number we can run
  private long m_currentRunnableNumber = 0;
  private final Object m_numberMutext = new Object();
  private final Object m_implementorsMutext = new Object();
  private final String m_name;
  private final Class<?> m_remoteClass;
  private final List<Object> m_implementors = new ArrayList<>();
  private final boolean m_singleThreaded;

  public EndPoint(final String name, final Class<?> remoteClass, final boolean singleThreaded) {
    m_name = name;
    m_remoteClass = remoteClass;
    m_singleThreaded = singleThreaded;
  }

  public Object getFirstImplementor() {
    synchronized (m_implementorsMutext) {
      if (m_implementors.size() != 1) {
        throw new IllegalStateException("Invalid implementor count, " + m_implementors);
      }
      return m_implementors.get(0);
    }
  }

  public long takeANumber() {
    return m_nextGivenNumber.getAndIncrement();
  }

  private void waitTillCanBeRun(final long number) {
    synchronized (m_numberMutext) {
      while (number > m_currentRunnableNumber) {
        try {
          m_numberMutext.wait();
        } catch (final InterruptedException e) {
          ClientLogger.logQuietly(e);
        }
      }
    }
  }

  private void releaseNumber() {
    synchronized (m_numberMutext) {
      m_currentRunnableNumber++;
      m_numberMutext.notifyAll();
    }
  }

  /**
   * @return is this the first implementor.
   */
  public boolean addImplementor(final Object implementor) {
    if (!m_remoteClass.isAssignableFrom(implementor.getClass())) {
      throw new IllegalArgumentException(m_remoteClass + " is not assignable from " + implementor.getClass());
    }
    synchronized (m_implementorsMutext) {
      final boolean isFirstImplementor = m_implementors.isEmpty();
      m_implementors.add(implementor);
      return isFirstImplementor;
    }
  }

  public boolean isSingleThreaded() {
    return m_singleThreaded;
  }

  public boolean hasImplementors() {
    synchronized (m_implementorsMutext) {
      return !m_implementors.isEmpty();
    }
  }

  public int getLocalImplementorCount() {
    synchronized (m_implementorsMutext) {
      return m_implementors.size();
    }
  }

  /**
   * @return we have no more implementors.
   */
  boolean removeImplementor(final Object implementor) {
    synchronized (m_implementorsMutext) {
      if (!m_implementors.remove(implementor)) {
        throw new IllegalStateException("Not removed, impl:" + implementor + " have " + m_implementors);
      }
      return m_implementors.isEmpty();
    }
  }

  public String getName() {
    return m_name;
  }

  public Class<?> getRemoteClass() {
    return m_remoteClass;
  }

  /*
   * @param number - like the number you get in a bank line, if we are single
   * threaded, then the method will not run until the number comes up. Acquire
   * with getNumber() @return a List of RemoteMethodCallResults
   */
  public List<RemoteMethodCallResults> invokeLocal(final RemoteMethodCall call, final long number,
      final INode messageOriginator) {
    try {
      if (m_singleThreaded) {
        waitTillCanBeRun(number);
      }
      return invokeMultiple(call, messageOriginator);
    } finally {
      releaseNumber();
    }
  }

  private List<RemoteMethodCallResults> invokeMultiple(final RemoteMethodCall call, final INode messageOriginator) {
    // copy the implementors
    final List<Object> implementorsCopy;
    synchronized (m_implementorsMutext) {
      implementorsCopy = new ArrayList<>(m_implementors);
    }
    final List<RemoteMethodCallResults> results = new ArrayList<>(implementorsCopy.size());
    for (final Object implementor : implementorsCopy) {
      results.add(invokeSingle(call, implementor, messageOriginator));
    }
    return results;
  }

  private RemoteMethodCallResults invokeSingle(final RemoteMethodCall call, final Object implementor,
      final INode messageOriginator) {
    call.resolve(m_remoteClass);
    final Method method;
    try {
      method = implementor.getClass().getMethod(call.getMethodName(), call.getArgTypes());
      method.setAccessible(true);
    } catch (final SecurityException | NoSuchMethodException e) {
      ClientLogger.logQuietly(e);
      throw new IllegalStateException(e.getMessage());
    }
    MessageContext.setSenderNodeForThread(messageOriginator);
    try {
      final Object methodRVal = method.invoke(implementor, call.getArgs());
      return new RemoteMethodCallResults(methodRVal);
    } catch (final InvocationTargetException e) {
      return new RemoteMethodCallResults(e.getTargetException());
    } catch (final IllegalAccessException e) {
      ClientLogger.logQuietly("error in call:" + call, e);
      return new RemoteMethodCallResults(e);
    } catch (final IllegalArgumentException e) {
      ClientLogger.logQuietly("error in call:" + call, e);
      return new RemoteMethodCallResults(e);
    } finally {
      MessageContext.setSenderNodeForThread(null);
    }
  }

  public boolean equivalent(final EndPoint other) {
    if (other.m_singleThreaded != this.m_singleThreaded) {
      return false;
    }
    if (!other.m_name.equals(this.m_name)) {
      return false;
    }
    return other.m_remoteClass.equals(m_remoteClass);
  }

  @Override
  public String toString() {
    return "Name:" + m_name + " singleThreaded:" + m_singleThreaded + " implementors:" + m_implementors;
  }
}
