// Copyright (c) 2011 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.core.model;

import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;

import org.chromium.debug.core.ChromiumDebugPlugin;
import org.chromium.debug.core.model.Variable.Real.HostObject;
import org.chromium.sdk.JsScope;
import org.chromium.sdk.JsValue;
import org.chromium.sdk.JsVariable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IValue;
import org.eclipse.debug.core.model.IVariable;

/**
 * A base implementation of all Chromium values. This class merely adds new interface,
 * not behavior. The actual class may be based on {@link JsValue} (regular case) or other data
 * sources.
 */
public abstract class ValueBase extends DebugElementImpl.WithEvaluate implements IValue {
  /**
   * Downcasts IValue to ValueBase if possible or returns null. This should be used in context
   * where IValue is available, but it's unknown whether it's Chromium value or not.
   * Clients should use this method rather that do instanceof themselves, because the latter is
   * unsafe technique (you cannot track manual downcasts and consequently cannot refactor/manage
   * them).
   */
  public static ValueBase cast(IValue value) {
    if (value instanceof ValueBase) {
      return (ValueBase) value;
    } else {
      return null;
    }
  }

  protected ValueBase(EvaluateContext evaluateContext) {
    super(evaluateContext);
  }

  /**
   * Downcasts to Value or return null.
   */
  public abstract Value asRealValue();

  public abstract String getValueString();

  /**
   * Represents a value as {@link HostObject}. This will be passed to value subproperties.
   */
  interface ValueAsHostObject extends Variable.Real.HostObject {
  }

  /**
   * A base implementation of Value that lazily calculates its inner variables.
   */
  public static abstract class ValueWithLazyVariables extends ValueBase {
    static final IVariable[] EMPTY_VARIABLES = new IVariable[0];

    private final AtomicReference<IVariable[]> variablesRef =
        new AtomicReference<IVariable[]>(null);

    protected ValueWithLazyVariables(EvaluateContext evaluateContext) {
      super(evaluateContext);
    }

    // This method could be blocking -- it gets called from a Worker thread.
    // All data should be prepared here.
    public IVariable[] getVariables() throws DebugException {
      try {
        // TODO: support clearing with cache clear.
        IVariable[] result = variablesRef.get();
        if (result != null) {
          return result;
        }
        IVariable[] variables = calculateVariables();
        variablesRef.compareAndSet(null, variables);
        return variablesRef.get();
      } catch (RuntimeException e) {
        // Log it, because Eclipse is likely to ignore it.
        ChromiumDebugPlugin.log(e);
        // We shouldn't throw RuntimeException from here, because calling
        // ElementContentProvider#update will forget to call update.done().
        throw new DebugException(new Status(IStatus.ERROR, ChromiumDebugPlugin.PLUGIN_ID,
            "Failed to read variables", e)); //$NON-NLS-1$
      }
    }

    // Consider making it more lazy for IIndexedValue implementation's benefit.
    protected abstract IVariable[] calculateVariables();
  }

  /**
   * Wraps {@link JsScope} as a Value. Scope's variables are inner variables of the Value.
   */
  static class ScopeValue extends ValueBase.ValueWithLazyVariables {
    private final JsScope jsScope;
    private final ValueBase.ValueAsHostObject selfAsHostObject;

    ScopeValue(EvaluateContext evaluateContext, JsScope jsScope,
        ValueBase.ValueAsHostObject selfAsHostObject) {
      super(evaluateContext);
      this.jsScope = jsScope;
      this.selfAsHostObject = selfAsHostObject;
    }
    @Override public String getReferenceTypeName() throws DebugException {
      return "#Scope";
    }
    @Override public String getValueString() {
      return null;
    }
    @Override public boolean isAllocated() throws DebugException {
      return true;
    }
    @Override public boolean hasVariables() throws DebugException {
      return true;
    }
    @Override public Value asRealValue() {
      return null;
    }

    @Override
    protected IVariable[] calculateVariables() {
      return StackFrame.wrapVariables(getEvaluateContext(), jsScope.getVariables(),
          Collections.<String>emptySet(), Collections.<JsVariable>emptyList(), selfAsHostObject,
          null);
    }
  }

  /**
   * Wraps string error message as a Value. The value has no inner variables.
   */
  static class ErrorMessageValue extends ValueBase {
    private final String message;

    ErrorMessageValue(EvaluateContext evaluateContext, String message) {
      super(evaluateContext);
      this.message = message;
    }
    @Override public String getReferenceTypeName() throws DebugException {
      return REFERENCE_TYPE_NAME;
    }
    @Override public String getValueString() {
      return message;
    }
    @Override public boolean isAllocated() throws DebugException {
      return true;
    }
    @Override public IVariable[] getVariables() throws DebugException {
      return Value.EMPTY_VARIABLES;
    }
    @Override public boolean hasVariables() throws DebugException {
      return false;
    }
    @Override public Value asRealValue() {
      return null;
    }

    private static final String REFERENCE_TYPE_NAME = "#Error Message";
  }
}