// Copyright (c) 2009 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.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.chromium.debug.core.util.JsValueStringifier;
import org.chromium.sdk.FunctionScopeExtension;
import org.chromium.sdk.JsArray;
import org.chromium.sdk.JsEvaluateContext;
import org.chromium.sdk.JsFunction;
import org.chromium.sdk.JsObject;
import org.chromium.sdk.JsValue;
import org.chromium.sdk.JsVariable;
import org.eclipse.debug.core.DebugException;
import org.eclipse.debug.core.model.IVariable;
import org.eclipse.debug.ui.IValueDetailListener;

/**
 * A generic (non-array) implementation of IValue using a JsValue instance.
 */
public class Value extends ValueBase.ValueWithLazyVariables {

  public static Value create(EvaluateContext evaluateContext, JsValue value,
      ValueBase.ValueAsHostObject hostObject) {
    if (JsValue.Type.TYPE_ARRAY == value.getType()) {
      return new ArrayValue(evaluateContext, (JsArray) value, hostObject);
    }
    return new Value(evaluateContext, value, hostObject);
  }

  private final JsValue value;

  private final ValueBase.ValueAsHostObject hostObject;

  private final DetailBuilder detailBuilder = new DetailBuilder();

  protected Value(EvaluateContext evaluateContext, JsValue value,
      ValueBase.ValueAsHostObject hostObject) {
    super(evaluateContext);
    this.value = value;
    this.hostObject = hostObject;
  }

  public String getReferenceTypeName() throws DebugException {
    return value.getType().toString();
  }

  public String getValueString() {
    String valueText = JsValueStringifier.toVisibleString(value);
    if (value.asObject() != null) {
      String ref = value.asObject().getRefId();
      if (ref != null) {
        valueText = valueText + "  (id=" + ref + ")";
      }
    }
    return valueText;
  }

  // This method could be blocking -- it gets called from a worker thread.
  // All data should be prepared here.
  protected IVariable[] calculateVariables() {
    JsObject asObject = value.asObject();
    if (asObject == null) {
      return EMPTY_VARIABLES;
    }
    List<Variable> functionScopes = calculateFunctionScopesVariable(asObject);
    return StackFrame.wrapVariables(getEvaluateContext(),
        asObject.getProperties(), Collections.<String>emptySet(),
        asObject.getInternalProperties(), hostObject, functionScopes);
  }

  /**
   * Returns 'function scopes' node packed as a list or an empty list if the input is not
   * a function. The 'function scopes' node holds actual scope variables as its children.
   * @return list of 0 or 1 elements
   */
  private List<Variable> calculateFunctionScopesVariable(JsObject jsObject) {
    JsFunction asFunction = jsObject.asFunction();
    if (asFunction == null) {
      return null;
    }
    FunctionScopeExtension functionScopeExtension =
        getConnectedData().getJavascriptVm().getFunctionScopeExtension();
    if (functionScopeExtension == null) {
      return null;
    }
    if (functionScopeExtension.getScopes(asFunction).isEmpty()) {
      return null;
    }
    Variable functionScopesVariable =
        Variable.forFunctionScopes(getEvaluateContext(), asFunction, functionScopeExtension);
    if (functionScopesVariable == null) {
      return null;
    }
    return Collections.singletonList(functionScopesVariable);
  }

  public boolean hasVariables() throws DebugException {
    return value.asObject() != null;
  }

  public boolean isAllocated() throws DebugException {
    return true;
  }

  @Override public Value asRealValue() {
    return this;
  }

  public JsValue getJsValue() {
    return value;
  }

  /**
   * Called from Worker thread.
   * @param listener will be called from various threads (its implementation is thread-safe)
   */
  public void computeDetailAsync(IValueDetailListener listener) {
    detailBuilder.buildDetailAsync(listener);
  }

  public boolean isTruncated() {
    return this.value.isTruncated() || detailBuilder.getCurrentDetailWrapper().isTruncated();
  }

  protected ValueBase.ValueAsHostObject getHostObject() {
    return hostObject;
  }

  public void reloadBiggerValue(final ReloadValueCallback callback) {
    List<JsValue> jsValueList = new ArrayList<JsValue>(2);
    if (this.value.isTruncated()) {
      jsValueList.add(this.value);
    }
    JsValue detailValue = detailBuilder.getCurrentDetailWrapper().getJsValue();
    if (detailValue != null && detailValue.isTruncated() && !jsValueList.contains(detailValue)) {
      jsValueList.add(detailValue);
    }

    ReloadBiggerValueProcess process = new ReloadBiggerValueProcess(callback);
    process.start(jsValueList);
  }

  public interface ReloadValueCallback {
    void done(boolean changed);
  }

  /**
   * An implementation of process that conducts reloading of several JsValue's.
   * We need such class because technically the Value contains the actual value and
   * a value of its detail (toString representation), both of which may have been truncated.
   */
  private static class ReloadBiggerValueProcess {
    private final ReloadValueCallback callback;
    private int counter;
    private boolean somethingChanged;

    ReloadBiggerValueProcess(ReloadValueCallback callback) {
      this.callback = callback;
    }

    void start(List<JsValue> jsValueList) {
      if (jsValueList.isEmpty()) {
        callback.done(false);
        return;
      }

      this.counter = jsValueList.size();
      this.somethingChanged = false;

      for (final JsValue jsValue : jsValueList) {
        final String originalValue = jsValue.getValueString();
        jsValue.reloadHeavyValue(new JsValue.ReloadBiggerCallback() {
              public void done() {
                String newValueString = jsValue.getValueString();
                boolean changed = newValueString != null && !newValueString.equals(originalValue);
                boolean weAreLast;
                boolean somethingChangedSaved;
                synchronized (ReloadBiggerValueProcess.this) {
                  counter--;
                  somethingChanged |= changed;
                  somethingChangedSaved = somethingChanged;
                  weAreLast = counter == 0;
                }
                if (weAreLast) {
                  callback.done(somethingChangedSaved);
                }
              }
            }, null);
      }
    }
  }

  /**
   * A small abstraction over detail value. Internally the value may be a plain string
   * or backed by a JsValue instance.
   */
  private abstract static class DetailWrapper {
    abstract boolean isTruncated();
    abstract JsValue getJsValue();
    abstract String getStringValue();
  }

  private static final DetailWrapper NO_DETAILS_WRAPPER = new DetailWrapper() {
    boolean isTruncated() {
      return false;
    }
    JsValue getJsValue() {
      return null;
    }
    String getStringValue() {
      return null;
    }
  };


  /**
   * Builds the string detail, possibly asynchronously. The details may be truncated
   * and reloaded in full later.
   */
  private class DetailBuilder {
    private volatile DetailWrapper detailWrapper = NO_DETAILS_WRAPPER;

    private static final String TO_STRING_ARGUMENT = "object";
    private static final String TO_STRING_EXPRESSION = "String(" + TO_STRING_ARGUMENT + ")";


    DetailWrapper getCurrentDetailWrapper() {
      return detailWrapper;
    }

    void buildDetailAsync(final IValueDetailListener listener) {
      DetailWrapper alreadyCalculatedDetail = this.detailWrapper;
      if (alreadyCalculatedDetail != NO_DETAILS_WRAPPER) {
        listener.detailComputed(Value.this, alreadyCalculatedDetail.getStringValue());
        return;
      }
      JsObject jsObject = getJsValue().asObject();
      if (jsObject == null) {
        jsValueDetailIsBuilt(getJsValue(), listener);
        return;
      }
      String objectRefId = jsObject.getRefId();
      if (objectRefId == null) {
        stringDetailIsBuilt("", listener);
        return;
      }

      if (getSuspendedState().isDismissed()) {
        stringDetailIsBuilt("", listener);
        return;
      }

      Map<String, String> additionalContext =
          Collections.singletonMap(TO_STRING_ARGUMENT, objectRefId);

      JsEvaluateContext.EvaluateCallback evaluateCallback =
          new JsEvaluateContext.EvaluateCallback() {
        public void success(JsVariable variable) {
          jsValueDetailIsBuilt(variable.getValue(), listener);
        }
        public void failure(String errorMessage) {
          stringDetailIsBuilt(errorMessage, listener);
        }
      };

      JsEvaluateContext evaluateContext =
          getSuspendedState().getDebugContext().getGlobalEvaluateContext();

      evaluateContext.evaluateAsync(TO_STRING_EXPRESSION, additionalContext,
          evaluateCallback, null);
    }

    private void stringDetailIsBuilt(final String detailString, IValueDetailListener listener) {
      DetailWrapper detailWrapper = new DetailWrapper() {
        boolean isTruncated() {
          return false;
        }
        String getStringValue() {
          return detailString;
        }
        JsValue getJsValue() {
          return null;
        }
      };
      detailIsBuiltImpl(detailWrapper, listener);
    }

    private void jsValueDetailIsBuilt(final JsValue detailValue, IValueDetailListener listener) {
      DetailWrapper detailWrapper = new DetailWrapper() {
        boolean isTruncated() {
          return detailValue.isTruncated();
        }
        String getStringValue() {
          return detailValue.getValueString();
        }
        JsValue getJsValue() {
          return detailValue;
        }
      };
      detailIsBuiltImpl(detailWrapper, listener);
    }

    private void detailIsBuiltImpl(DetailWrapper detailWrapper, IValueDetailListener listener) {
      // We may override value concurrently, but it's ok.
      this.detailWrapper = detailWrapper;
      listener.detailComputed(Value.this, detailWrapper.getStringValue());
    }
  }
}
