// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.sdk.tests.system;

import org.chromium.sdk.DebugContext;
import org.chromium.sdk.DebugEventListener;
import org.chromium.sdk.Script;
import org.chromium.sdk.TabDebugEventListener;

/**
 * Listens to various asynchronous debug events and let main thread to approve them and
 * synchronize with them. Unless {@link #setDefaultReceiver} is called,
 * listener always blocks waiting for the main thread to synchronize by {@link #expectEvent}.
 */
class StateManager {
  private final StateManager.Monitor monitor = new Monitor();

  TabDebugEventListener getTabListener() {
    return tabDebugEventListener;
  }

  /**
   * Called from the main thread. Waits for a particular event to happen and returns its data.
   * The event mask is specified by receiver parameter. Receiver has several visit* methods
   * with the similar semantics: it returns null if event should be skipped, non-null if
   * event is accepted and the data should be passed to the main thread function or may throw
   * an exception if test assumptions are broken.
   */
  <RES> RES expectEvent(EventVisitor<RES> receiver) throws SmokeException {
    RES res;
    do {
      res = expectOneEvent(receiver);
    } while (res == null);
    return res;
  }
  private <RES> RES expectOneEvent(EventVisitor<RES> receiver) throws SmokeException {
    synchronized (monitor) {
      if (monitor.pendingException != null) {
        throw new SmokeException(monitor.pendingException);
      }
      monitor.defaultReceiver = null;
      if (monitor.pendingEvent == null) {
        try {
          monitor.wait(TIMEOUT_MS);
        } catch (InterruptedException e) {
          throw new RuntimeException(e);
        }
        if (monitor.pendingEvent == null) {
          throw new SmokeException("Timeout waiting for event with " + receiver);
        }
      }
      RES res = monitor.pendingEvent.accept(receiver);
      monitor.pendingEvent = null;
      monitor.notify();
      return res;
    }
  }

  /**
   * Called from the main thread. Allows non-blocking processing of events in listener.
   * The default receiver accepts all events but its return value is ignored unless it threw an
   * exception.
   */
  void setDefaultReceiver(EventVisitor<?> receiver) throws SmokeException {
    synchronized (monitor) {
      if (monitor.pendingException != null) {
        throw new SmokeException(monitor.pendingException);
      }
      monitor.defaultReceiver = receiver;

      if (receiver != null && monitor.pendingEvent != null) {
        monitor.pendingEvent.accept(receiver);
        monitor.pendingEvent = null;
        monitor.notify();
      }
    }
  }

  /**
   * Called from Dispatch thread by listener implementation.
   */
  private void processEvent(Event event) {
    synchronized (monitor) {
      if (monitor.defaultReceiver != null) {
        try {
          event.accept(monitor.defaultReceiver);
        } catch (SmokeException e) {
          monitor.pendingException  = e;
        }
        return;
      }
      monitor.pendingEvent = event;
      monitor.notify();
      try {
        monitor.wait(TIMEOUT_MS);
        if (monitor.pendingEvent != null) {
          throw new RuntimeException("Timeout waiting for event processing");
        }
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
  }

  private final TabDebugEventListener tabDebugEventListener = new TabDebugEventListener() {
    public void closed() {
      processEvent(new Event() {
        public <RES> RES accept(EventVisitor<RES> visitor) throws SmokeException {
          return visitor.visitClosed();
        }
      });
    }
    public void navigated(final String newUrl) {
      processEvent(new Event() {
        public <RES> RES accept(EventVisitor<RES> visitor) throws SmokeException {
          return visitor.visitNavigated(newUrl);
        }
      });
    }
    public DebugEventListener getDebugEventListener() {
      return debugEventListener;
    }
  };
  private final DebugEventListener debugEventListener = new DebugEventListener() {
    public void disconnected() {
      processEvent(new Event() {
        public <RES> RES accept(EventVisitor<RES> visitor) throws SmokeException {
          return visitor.visitDisconnected();
        }
      });
    }

    public void resumed() {
      processEvent(new Event() {
        public <RES> RES accept(EventVisitor<RES> visitor) throws SmokeException {
          return visitor.visitResumed();
        }
      });
    }

    public void scriptLoaded(final Script newScript) {
      processEvent(new Event() {
        public <RES> RES accept(EventVisitor<RES> visitor) throws SmokeException {
          return visitor.visitScriptLoaded(newScript);
        }
      });
    }

    public void scriptCollected(final Script script) {
      processEvent(new Event() {
        public <RES> RES accept(EventVisitor<RES> visitor) throws SmokeException {
          return visitor.visitScriptCollected(script);
        }
      });
    }

    public void suspended(final DebugContext context) {
      processEvent(new Event() {
        public <RES> RES accept(EventVisitor<RES> visitor) throws SmokeException {
          return visitor.visitSuspended(context);
        }
      });
    }

    public VmStatusListener getVmStatusListener() {
      return null;
    }

    public void scriptContentChanged(Script newScript) {
    }
  };

  /**
   * An internal implementation of event from {@link DebugEventListener}
   * and {@link TabDebugEventListener}.
   */
  private interface Event {
    <RES> RES accept(EventVisitor<RES> visitor) throws SmokeException;
  }

  private static class Monitor {
    EventVisitor<?> defaultReceiver = null;
    Event pendingEvent = null;
    SmokeException pendingException = null;
  }

  private static final int TIMEOUT_MS = 10000;
}