// 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.sdk.internal.v8native.value;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.chromium.sdk.JsObject;
import org.chromium.sdk.JsScope;
import org.chromium.sdk.JsValue;
import org.chromium.sdk.JsVariable;
import org.chromium.sdk.internal.v8native.CallFrameImpl;
import org.chromium.sdk.internal.v8native.InternalContext;
import org.chromium.sdk.internal.v8native.protocol.V8ProtocolUtil;
import org.chromium.sdk.internal.v8native.protocol.input.ScopeRef;
import org.chromium.sdk.internal.v8native.protocol.input.data.ObjectValueHandle;
import org.chromium.sdk.internal.v8native.protocol.output.DebuggerMessageFactory;
import org.chromium.sdk.util.AsyncFuture;
import org.chromium.sdk.util.AsyncFuture.SyncOperation;
import org.chromium.sdk.util.MethodIsBlockingException;

/**
 * A generic implementation of the JsScope interface.
 */
public abstract class JsScopeImpl<D> implements JsScope {

  /**
   * An abstraction over object that hosts the scope. It could be either call frame or function.
   */
  public static abstract class Host {
    public static Host create(final CallFrameImpl callFrameImpl) {
      return new Host() {
        @Override InternalContext getInternalContext() {
          return callFrameImpl.getInternalContext();
        }
        @Override DebuggerMessageFactory.ScopeHostParameter getFactoryParameter() {
          return DebuggerMessageFactory.ScopeHostParameter.forFrame(callFrameImpl.getIdentifier());
        }
      };
    }

    public static Host create(final JsFunctionImpl jsFunctionImpl) {
      return new Host() {
        @Override InternalContext getInternalContext() {
          return jsFunctionImpl.getInternalContext();
        }
        @Override DebuggerMessageFactory.ScopeHostParameter getFactoryParameter() {
          return DebuggerMessageFactory.ScopeHostParameter.forFunction(jsFunctionImpl.getRef());
        }
      };
    }

    abstract InternalContext getInternalContext();

    abstract DebuggerMessageFactory.ScopeHostParameter getFactoryParameter();
  }

  private final Host host;
  private final int scopeIndex;
  private final Type type;
  private final AtomicReference<AsyncFuture<D>> deferredDataRef =
      new AtomicReference<AsyncFuture<D>>(null);

  public static JsScopeImpl<?> create(Host host, ScopeRef scopeRef) {
    Type type = convertType((int) scopeRef.type());
    if (type == Type.WITH) {
      return new With(host, type, (int) scopeRef.index());
    } else {
      return new NoWith(host, type, (int) scopeRef.index());
    }
  }

  protected JsScopeImpl(Host host, Type type, int scopeIndex) {
    this.host = host;
    this.type = type;
    this.scopeIndex = scopeIndex;
  }

  @Override
  public Type getType() {
    return type;
  }

  @Override
  public List<? extends JsVariable> getVariables() throws MethodIsBlockingException {
    return getVariables(getDeferredData());
  }

  protected D getDeferredData() throws MethodIsBlockingException {
    AsyncFuture<D> future = deferredDataRef.get();
    ValueLoaderImpl valueLoader = host.getInternalContext().getValueLoader();
    int cacheState = valueLoader.getCurrentCacheState();
    boolean restartOperation;
    if (future == null) {
      // Do not restart operation if other thread has already started it.
      restartOperation = false;
    } else {
      D result = future.getSync();
      int dataCacheState = getDataCacheState(result);
      if (dataCacheState == cacheState) {
        return result;
      }
      restartOperation = true;
    }
    SyncOperation<D> loadOperation = createLoadDataOperation(valueLoader, cacheState);
    // Create future, so that other threads didn't start operations of their own.
    AsyncFuture.initializeReference(deferredDataRef, loadOperation.asAsyncOperation(),
        restartOperation);
    loadOperation.execute();
    return deferredDataRef.get().getSync();
  }

  protected abstract SyncOperation<D> createLoadDataOperation(ValueLoaderImpl valueLoader,
      int cacheState);

  protected abstract List<? extends JsVariable> getVariables(D data)
      throws MethodIsBlockingException;

  protected abstract int getDataCacheState(D data);

  protected ObjectValueHandle loadScopeObject(ValueLoaderImpl valueLoader)
      throws MethodIsBlockingException {
    return valueLoader.loadScopeFields(scopeIndex, host.getFactoryParameter());
  }

  public static Type convertType(int typeCode) {
    Type type = CODE_TO_TYPE.get(typeCode);
    if (type == null) {
      type = Type.UNKNOWN;
    }
    return type;
  }

  private static class NoWith extends JsScopeImpl<NoWith.DeferredData> {
    NoWith(Host host, Type type, int scopeIndex) {
      super(host, type, scopeIndex);
    }

    @Override
    public WithScope asWithScope() {
      return null;
    }

    @Override
    protected List<? extends JsVariable> getVariables(DeferredData data) {
      return data.variables;
    }

    @Override
    protected int getDataCacheState(DeferredData data) {
      return data.cacheState;
    }

    @Override
    protected SyncOperation<DeferredData> createLoadDataOperation(
        final ValueLoaderImpl valueLoader, final int cacheState) {
      return new SyncOperation<DeferredData>() {
        @Override
        protected DeferredData runSync() throws MethodIsBlockingException {
          List<JsVariable> list = load(valueLoader);
          return new DeferredData(list, cacheState);
        }
      };
    }

    private List<JsVariable> load(ValueLoaderImpl valueLoader)
        throws MethodIsBlockingException {
      ObjectValueHandle scopeObject = loadScopeObject(valueLoader);
      if (scopeObject == null) {
        return Collections.emptyList();
      }
      List<? extends PropertyReference> propertyRefs =
          V8ProtocolUtil.extractObjectProperties(scopeObject);

      List<ValueMirror> propertyMirrors = valueLoader.getOrLoadValueFromRefs(propertyRefs);

      List<JsVariable> properties = new ArrayList<JsVariable>(propertyMirrors.size());
      for (int i = 0; i < propertyMirrors.size(); i++) {
        // This name should be string. We are making it string as a fall-back strategy.
        String varNameStr = propertyRefs.get(i).getName().toString();
        properties.add(new JsVariableImpl(valueLoader, propertyMirrors.get(i), varNameStr));
      }
      return properties;
    }

    static class DeferredData {
      final List<? extends JsVariable> variables;
      final int cacheState;

      DeferredData(List<? extends JsVariable> variables, int cacheState) {
        this.variables = variables;
        this.cacheState = cacheState;
      }
    }
  }

  private static class With extends JsScopeImpl<With.DeferredData> implements JsScope.WithScope {
    With(Host host, Type type, int scopeIndex) {
      super(host, type, scopeIndex);
    }

    @Override
    public WithScope asWithScope() {
      return this;
    }

    @Override
    public JsValue getWithArgument() throws MethodIsBlockingException {
      return getDeferredData().jsValue;
    }

    @Override
    protected SyncOperation<DeferredData> createLoadDataOperation(
        final ValueLoaderImpl valueLoader, final int cacheState) {
      return new SyncOperation<DeferredData>() {
        @Override
        protected DeferredData runSync() throws MethodIsBlockingException {
          return load(valueLoader, cacheState);
        }
      };
    }

    private DeferredData load(ValueLoaderImpl valueLoader, int cacheState)
        throws MethodIsBlockingException {
      ObjectValueHandle scopeObject = loadScopeObject(valueLoader);
      ValueMirror mirror = valueLoader.addDataToMap(scopeObject.getSuper());
      JsValue jsValue = JsVariableImpl.createValue(valueLoader, mirror, "<with object>");
      return new DeferredData(jsValue, cacheState);
    }

    @Override
    protected List<? extends JsVariable> getVariables(DeferredData data)
        throws MethodIsBlockingException {
      if (data.orderedProperties == null) {
        List<? extends JsVariable> list;
        JsObject jsObject = data.jsValue.asObject();
        if (jsObject == null) {
          list = Collections.emptyList();
        } else {
          list = new ArrayList<JsVariable>(jsObject.getProperties());
        }
        data.orderedProperties = list;
      }
      return data.orderedProperties;
    }

    @Override
    protected int getDataCacheState(DeferredData data) {
      return data.cacheState;
    }

    static class DeferredData {
      final JsValue jsValue;
      final int cacheState;

      // Stores properties in the list -- the order is unspecified, but fixed.
      volatile List<? extends JsVariable> orderedProperties = null;

      DeferredData(JsValue jsValue, int cacheState) {
        this.jsValue = jsValue;
        this.cacheState = cacheState;
      }
    }
  }

  private static final Map<Integer, Type> CODE_TO_TYPE;
  static {
    CODE_TO_TYPE = new HashMap<Integer, Type>();
    CODE_TO_TYPE.put(0, Type.GLOBAL);
    CODE_TO_TYPE.put(1, Type.LOCAL);
    CODE_TO_TYPE.put(2, Type.WITH);
    CODE_TO_TYPE.put(3, Type.CLOSURE);
    CODE_TO_TYPE.put(4, Type.CATCH);
  }
}
