// 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.debug.ui.liveedit;

import static org.chromium.debug.ui.DialogUtils.createErrorOptional;
import static org.chromium.debug.ui.DialogUtils.createOptional;

import org.chromium.debug.core.model.PushChangesPlan;
import org.chromium.debug.ui.DialogUtils.Message;
import org.chromium.debug.ui.DialogUtils.MessagePriority;
import org.chromium.debug.ui.DialogUtils.Optional;
import org.chromium.debug.ui.DialogUtils.Scope;
import org.chromium.debug.ui.DialogUtils.Updater;
import org.chromium.debug.ui.DialogUtils.ValueConsumer;
import org.chromium.debug.ui.DialogUtils.ValueSource;
import org.chromium.sdk.UpdatableScript;
import org.chromium.sdk.UpdatableScript.ChangeDescription;
import org.eclipse.osgi.util.NLS;

/**
 * An asynchronous loader of LiveEdit update preview data. It deals with outer world in terms of
 * {@link Updater} sources/consumers.
 * It implements {@link ValueSource} interface that provides a loaded result, or null data
 * if the result is not loaded yet. Updater gets notified whenever result is delivered.
 * <p>The input parameter may be changed at any moment.
 * <p>The loader may be in active or passive state. Since the preview data this class loads
 * is optional and is not required for wizard's work, the loader should be kept passive until
 * user actually needs its result (turns the page).
 */
class PreviewLoader implements ValueSource<Optional<PreviewLoader.Data>> {
  private final Updater updater;
  private final ValueSource<PushChangesPlan> inputParameterSource;
  private boolean active = false;
  private final Monitor dataMonitor = new Monitor();

  PreviewLoader(Updater updater, ValueSource<PushChangesPlan> inputParameterSource) {
    this.updater = updater;
    this.inputParameterSource = inputParameterSource;
  }

  void registerSelf(Scope scope) {
    updater.addSource(scope, this);
    updater.addConsumer(scope, parametersConsumer);
    updater.addDependency(parametersConsumer, inputParameterSource);
  }

  void setActive(boolean active) {
    this.active = active;
    if (active) {
      requestPreview();
    }
  }

  private void requestPreview() {
    final PushChangesPlan plan = inputParameterSource.getValue();
    boolean inputIsNew = dataMonitor.updateInputAndStarted(plan);
    if (!inputIsNew) {
      return;
    }

    // Report about our value becoming empty.
    updater.reportChanged(this);

    UpdatableScript.UpdateCallback callback = new UpdatableScript.UpdateCallback() {
      public void failure(String message) {
        Optional<Data> error = createErrorOptional(
            new Message(NLS.bind(Messages.PreviewLoader_FAILED_TO_GET, message),
                MessagePriority.WARNING));
        done(error);
      }
      public void success(Object report,
          final UpdatableScript.ChangeDescription changeDescription) {
        Optional<Data> result;
        if (changeDescription == null) {
          result = EMPTY_DATA;
        } else {
          Data data = new Data() {
            @Override public PushChangesPlan getChangesPlan() {
              return plan;
            }
            @Override public ChangeDescription getChangeDescription() {
              return changeDescription;
            }
          };
          result = createOptional(data);
        }
        done(result);
      }
      private void done(Optional<Data> result) {
        boolean resultTaken = dataMonitor.updateResult(result, plan);
        if (resultTaken) {
          updater.reportChanged(PreviewLoader.this);
          updater.updateAsync();
        }
      }
    };

    plan.execute(true, callback, null);
  }

  public interface Data {
    UpdatableScript.ChangeDescription getChangeDescription();
    PushChangesPlan getChangesPlan();
  }

  public Optional<Data> getValue() {
    return dataMonitor.getValue();
  }

  // A consumer that receive the actual input parameter (ScriptTargetMapping).
  private final ValueConsumer parametersConsumer = new ValueConsumer() {
    public void update(Updater updater) {
      PushChangesPlan inputPlan = inputParameterSource.getValue();
      boolean updated = dataMonitor.updateInput(inputPlan);
      if (updated && active) {
        requestPreview();
      }
    }
  };

  // Makes sure that some fields are only accessed in synchronized fashion.
  private static class Monitor {
    private PushChangesPlan input = null;
    private boolean alreadyStarted = false;
    private Optional<Data> result = NO_DATA;

    synchronized boolean updateInputAndStarted(PushChangesPlan inputPlan) {
      if (inputPlan == input) {
        if (alreadyStarted) {
          return false;
        } else {
          alreadyStarted = true;
          return true;
        }
      } else {
        input = inputPlan;
        result = NO_DATA;
        alreadyStarted = true;
        return true;
      }
    }

    synchronized boolean updateInput(PushChangesPlan inputPlan) {
      if (inputPlan == input) {
        return false;
      } else {
        input = inputPlan;
        result = NO_DATA;
        alreadyStarted = false;
        return true;
      }
    }

    synchronized Optional<Data> getValue() {
      return result;
    }

    synchronized boolean updateResult(Optional<Data> result,
        PushChangesPlan inputPlan) {
      if (inputPlan != input) {
        return false;
      }
      this.result = result;
      return true;
    }
  }

  private static final Optional<Data> NO_DATA = createErrorOptional(
      new Message(Messages.PreviewLoader_WAITING_FOR_DIFF, MessagePriority.NONE));
  private static final Optional<Data> EMPTY_DATA =
      createErrorOptional(new Message(Messages.PreviewLoader_FAILED_TO_LOAD, MessagePriority.NONE));
}
