// 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.Collection;
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.JsFunction;
import org.chromium.sdk.JsObject;
import org.chromium.sdk.JsVariable;
import org.chromium.sdk.internal.v8native.InternalContext;
import org.chromium.sdk.util.AsyncFuture;
import org.chromium.sdk.util.MethodIsBlockingException;

/**
 * A generic implementation of the JsObject interface.
 * @param <D> type of convenience user-provided property-data-holding structure, that always
 *     includes {@link BasicPropertyData} but may also contain other fields that expire when
 *     properties expire
 */
public abstract class JsObjectBase<D> extends JsValueBase implements JsObject {

  private final long ref;

  private final String className;

  private final ValueLoader valueLoader;

  /**
   * Fully qualified name of variable holding this object.
   */
  private final String variableFqn;

  /**
   * Property data in form of {@link AsyncFuture} for property load operation
   * that several threads may access simultaneously. The future gets reinitialized
   * on the next access after cache state was updated.
   */
  private final AtomicReference<AsyncFuture<D>> propertyDataRef =
      new AtomicReference<AsyncFuture<D>>(null);

  /**
   * This constructor implies the lazy resolution of object properties.
   *
   * @param valueLoader where this instance belongs in
   * @param variableFqn the fully qualified name of the variable holding this object
   * @param valueState the value data from the JS VM
   */
  JsObjectBase(ValueLoader valueLoader, String variableFqn, ValueMirror mirror) {
    super(mirror);
    this.valueLoader = valueLoader;
    this.ref = mirror.getRef();
    this.className = mirror.getClassName();
    this.variableFqn = variableFqn;
  }

  @Override
  public Collection<JsVariableImpl> getProperties() throws MethodIsBlockingException {
    return getBasicPropertyData(true).getPropertyList();
  }

  @Override
  public Collection<JsVariableImpl> getInternalProperties() throws MethodIsBlockingException {
    return getBasicPropertyData(true).getIntenalPropertyList();
  }

  @Override
  public String getRefId() {
    if (ref < 0) {
      // Negative handle means that it's transient. We don't expose it.
      return null;
    } else {
      return String.valueOf(ref);
    }
  }

  @Override
  public ValueLoader getRemoteValueMapping() {
    return valueLoader;
  }

  public static int parseRefId(String value) {
    return Integer.parseInt(value);
  }

  @Override
  public JsObjectBase<D> asObject() {
    return this;
  }

  @Override
  public JsVariable getProperty(String name) throws MethodIsBlockingException {
    return getBasicPropertyData(true).getPropertyMap().get(name);
  }

  @Override
  public String getClassName() {
    return className;
  }

  @Override
  public String getValueString() {
    switch (getType()) {
      case TYPE_OBJECT:
      case TYPE_ARRAY:
        return "[" + getClassName() + "]";
      case TYPE_FUNCTION:
        return "[Function]";
      default:
        return "";
    }
  }

  protected InternalContext getInternalContext() {
    return valueLoader.getInternalContext();
  }

  protected long getRef() {
    return ref;
  }

  /**
   * Gets or creates property data. The data have cache timestamp inside and gets recreated
   * if checkFreshness is true and if global timestamp has been updated.
   * @param checkFreshness whether data freshness is to be check against global cache timestamp
   * @return property data wrapped in convenience class as specified by
   *     {@link #wrapBasicData(BasicPropertyData)} method
   */
  protected D getPropertyData(boolean checkFreshness) throws MethodIsBlockingException {
    if (propertyDataRef.get() == null) {
      int currentCacheState = getRemoteValueMapping().getCurrentCacheState();
      startPropertyLoadOperation(false, currentCacheState);
    } else {
      if (checkFreshness) {
        int currentCacheState = getRemoteValueMapping().getCurrentCacheState();
        D result = propertyDataRef.get().getSync();
        BasicPropertyData basicPropertyData = unwrapBasicData(result);
        if (basicPropertyData.getCacheState() == currentCacheState) {
          return result;
        }
        startPropertyLoadOperation(true, currentCacheState);
      }
    }

    return propertyDataRef.get().getSync();
  }

  /**
   * Convenience method that gets property data and returns wrapped {@link BasicPropertyData}.
   */
  protected BasicPropertyData getBasicPropertyData(boolean checkFreshness)
      throws MethodIsBlockingException {
    D propertyData = getPropertyData(checkFreshness);
    return unwrapBasicData(propertyData);
  }

  private void startPropertyLoadOperation(boolean reload, final int currentCacheState)
      throws MethodIsBlockingException {
    // The operation is blocking, because we will wait for its result anyway.
    // On the other hand there is a post-load job that we need a thread to occupy with.

    AsyncFuture.SyncOperation<D> blockingOperation =
        new AsyncFuture.SyncOperation<D>() {
      @Override
      protected D runSync() throws MethodIsBlockingException {
        SubpropertiesMirror subpropertiesMirror =
            getRemoteValueMapping().getOrLoadSubproperties(ref);

        List<JsVariableImpl> properties = wrapProperties(subpropertiesMirror.getProperties());
        List<JsVariableImpl> internalProperties =
            wrapProperties(subpropertiesMirror.getInternalProperties());

        BasicPropertyData data = new BasicPropertyData(currentCacheState, properties,
            internalProperties, subpropertiesMirror);
        return wrapBasicData(data);
      }

      private List<JsVariableImpl> wrapProperties(List<? extends PropertyReference> propertyRefs)
          throws MethodIsBlockingException {
        List<ValueMirror> subMirrors = valueLoader.getOrLoadValueFromRefs(propertyRefs);

        List<JsVariableImpl> wrappedProperties = createPropertiesFromMirror(subMirrors,
            propertyRefs);
        return Collections.unmodifiableList(wrappedProperties);
      }
    };
    if (reload) {
      AsyncFuture.reinitializeReference(propertyDataRef, blockingOperation.asAsyncOperation());
    } else {
      AsyncFuture.initializeReference(propertyDataRef, blockingOperation.asAsyncOperation());
    }
    blockingOperation.execute();
  }

  /**
   * User-provided method that wraps basic property data in the class of user choice D.
   * User wrapper will be kept by {@link JsObjectBase} and easily accessible when needed,
   * plus it will be dropped when caches become reset.
   * <p>
   * Alternative design would be to require D to extend BasicPropertyData, but we would have
   * to expose all of its constructor parameters in this case.
   */
  protected abstract D wrapBasicData(BasicPropertyData basicPropertyData);

  /**
   * User-provided method that extracts basic property data from user-provided data class.
   */
  protected abstract BasicPropertyData unwrapBasicData(D wrappedBasicData);

  /**
   * Contains immutable data about object properties plus lazy-initialized fields that are
   * derived from property data. There can be more fields in user-provided wrapper class.
   */
  protected static class BasicPropertyData {
    private final int cacheState;

    private final List<JsVariableImpl> propertyList;
    private final List<JsVariableImpl> intenalPropertyList;
    private final SubpropertiesMirror subpropertiesMirror;

    private volatile Map<String, JsVariableImpl> propertyMap = null;

    BasicPropertyData(int cacheState,
        List<JsVariableImpl> propertyList,
        List<JsVariableImpl> intenalPropertyList, SubpropertiesMirror subpropertiesMirror) {
      this.cacheState = cacheState;
      this.propertyList = propertyList;
      this.intenalPropertyList = intenalPropertyList;
      this.subpropertiesMirror = subpropertiesMirror;
    }

    int getCacheState() {
      return cacheState;
    }

    List<JsVariableImpl> getPropertyList() {
      return propertyList;
    }

    List<JsVariableImpl> getIntenalPropertyList() {
      return intenalPropertyList;
    }

    SubpropertiesMirror getSubpropertiesMirror() {
      return subpropertiesMirror;
    }

    Map<String, JsVariableImpl> getPropertyMap() {
      // Method is not synchronized -- it's OK if we initialize volatile propertyMap field
      // several times.
      if (propertyMap == null) {
        Map<String, JsVariableImpl> map =
            new HashMap<String, JsVariableImpl>(propertyList.size() * 2, 0.75f);
        for (JsVariableImpl prop : propertyList) {
          map.put(prop.getName(), prop);
        }
        // Make make synchronized for such not thread-safe methods as entrySet.
        propertyMap = Collections.unmodifiableMap(Collections.synchronizedMap(map));
      }
      return propertyMap;
    }
  }

  private List<JsVariableImpl> createPropertiesFromMirror(List<ValueMirror> mirrorProperties,
      List<? extends PropertyReference> propertyRefs) {
    // TODO(peter.rybin) Maybe assert that context is valid here

    List<JsVariableImpl> result = new ArrayList<JsVariableImpl>(mirrorProperties.size());
    for (int i = 0; i < mirrorProperties.size(); i++) {
      ValueMirror mirror = mirrorProperties.get(i);
      Object varName = propertyRefs.get(i).getName();
      String fqn = getFullyQualifiedName(varName);
      if (fqn == null) {
        continue;
      }
      String decoratedName = JsVariableImpl.NameDecorator.decorateVarName(varName);
      result.add(new JsVariableImpl(valueLoader, mirror, varName, decoratedName, fqn));
    }
    return result;
  }

  private String getFullyQualifiedName(Object propName) {
    if (variableFqn == null) {
      return null;
    }
    if (propName instanceof String) {
      String propNameStr = (String) propName;
      if (propNameStr.startsWith(".")) {
        // ".arguments" is not legal
        return null;
      }
    }
    return variableFqn + JsVariableImpl.NameDecorator.buildAccessSuffix(propName);
  }

  public static class Impl extends JsObjectBase<BasicPropertyData> {
    Impl(ValueLoader valueLoader, String variableFqn, ValueMirror valueState) {
      super(valueLoader, variableFqn, valueState);
    }

    @Override
    public JsArrayImpl asArray() {
      return null;
    }

    @Override
    public JsFunction asFunction() {
      return null;
    }

    @Override
    public String toString() {
      StringBuilder result = new StringBuilder();
      result.append("[JsObject: type=").append(getType());
      try {
        for (JsVariable prop : getProperties()) {
          result.append(',').append(prop);
        }
      } catch (MethodIsBlockingException e) {
        return "[JsObject: Exception in retrieving data]";
      }
      result.append(']');
      return result.toString();
    }

    @Override
    protected BasicPropertyData wrapBasicData(BasicPropertyData basicPropertyData) {
      return basicPropertyData;
    }

    @Override
    protected BasicPropertyData unwrapBasicData(BasicPropertyData additionalPropertyStore) {
      return additionalPropertyStore;
    }
  }
}
