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

import static org.chromium.sdk.util.BasicUtil.getSafe;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.chromium.sdk.JsValue;
import org.chromium.sdk.RelayOk;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.internal.JsonUtil;
import org.chromium.sdk.internal.protocolparser.JsonProtocolParseException;
import org.chromium.sdk.internal.v8native.DebugSession;
import org.chromium.sdk.internal.v8native.InternalContext;
import org.chromium.sdk.internal.v8native.V8BlockingCallback;
import org.chromium.sdk.internal.v8native.V8CommandCallbackBase;
import org.chromium.sdk.internal.v8native.V8Helper;
import org.chromium.sdk.internal.v8native.InternalContext.ContextDismissedCheckedException;
import org.chromium.sdk.internal.v8native.protocol.input.ScopeBody;
import org.chromium.sdk.internal.v8native.protocol.input.SuccessCommandResponse;
import org.chromium.sdk.internal.v8native.protocol.input.V8ProtocolParserAccess;
import org.chromium.sdk.internal.v8native.protocol.input.data.ObjectValueHandle;
import org.chromium.sdk.internal.v8native.protocol.input.data.RefWithDisplayData;
import org.chromium.sdk.internal.v8native.protocol.input.data.SomeHandle;
import org.chromium.sdk.internal.v8native.protocol.input.data.ValueHandle;
import org.chromium.sdk.internal.v8native.protocol.output.DebuggerMessage;
import org.chromium.sdk.internal.v8native.protocol.output.DebuggerMessageFactory;
import org.chromium.sdk.internal.v8native.protocol.output.LookupMessage;
import org.chromium.sdk.util.GenericCallback;
import org.chromium.sdk.util.MethodIsBlockingException;
import org.json.simple.JSONObject;

/**
 * Implementation of {@link ValueLoader} that loads data by 'lookup' command and additionally
 * collects data as reported by various addDataToMap methods.
 * <p>V8 typically sends a lot of (unsolicited) data about properties. There could be various
 * strategies about whether to parse and add them into a map or save parsing time and ignore.
 */
public class ValueLoaderImpl extends ValueLoader {

  private final ConcurrentMap<Long, ValueMirror> refToMirror =
      new ConcurrentHashMap<Long, ValueMirror>();

  private final HandleManager specialHandleManager = new HandleManager();

  private final InternalContext context;
  private final LoadableString.Factory loadableStringFactory;

  private final AtomicInteger cacheStateRef = new AtomicInteger(1);

  public ValueLoaderImpl(InternalContext context) {
    this.context = context;
    this.loadableStringFactory = new StringFactory();
  }

  public LoadableString.Factory getLoadableStringFactory() {
    return loadableStringFactory;
  }

  public HandleManager getSpecialHandleManager() {
    return specialHandleManager;
  }

  @Override
  public void clearCaches() {
    cacheStateRef.incrementAndGet();
    refToMirror.clear();
  }

  @Override
  int getCurrentCacheState() {
    return cacheStateRef.get();
  }

  @Override
  public InternalContext getInternalContext() {
    return context;
  }

  public void addHandleFromRefs(SomeHandle handle) {
    if (HandleManager.isSpecialType(handle.type())) {
      specialHandleManager.put(handle.handle(), handle);
    } else {
      ValueHandle valueHandle;
      try {
        valueHandle = handle.asValueHandle();
      } catch (JsonProtocolParseException e) {
        throw new RuntimeException(e);
      }
      addDataToMap(valueHandle);
    }
  }

  public ValueMirror addDataToMap(RefWithDisplayData refWithDisplayData) {
    ValueMirror mirror = ValueMirror.create(refWithDisplayData, getLoadableStringFactory());
    return putValueMirrorIntoMapRecursive(mirror);
  }

  public ValueMirror addDataToMap(ValueHandle valueHandle) {
    ValueMirror mirror = ValueMirror.create(valueHandle, getLoadableStringFactory());
    return putValueMirrorIntoMapRecursive(mirror);
  }

  public ValueMirror addDataToMap(Long ref, JsValue.Type type, String className,
      LoadableString loadableString, SubpropertiesMirror subpropertiesMirror) {
    ValueMirror mirror =
        ValueMirror.create(ref, type, className, loadableString, subpropertiesMirror);
    return putValueMirrorIntoMapRecursive(mirror);
  }

  private ValueMirror putValueMirrorIntoMapRecursive(ValueMirror mirror) {
    if (PRE_PARSE_PROPERTIES) {
      SubpropertiesMirror subpropertiesMirror = mirror.getProperties();
      if (subpropertiesMirror != null) {
        subpropertiesMirror.reportAllProperties(this);
      }
    }
    return mergeValueMirrorIntoMap(mirror.getRef(), mirror);
  }

  private ValueMirror mergeValueMirrorIntoMap(Long ref, ValueMirror mirror) {
    while (true) {
      ValueMirror old = refToMirror.putIfAbsent(ref, mirror);
      if (old == null) {
        return mirror;
      }
      ValueMirror merged = ValueMirror.merge(old, mirror);
      if (merged == old) {
        return merged;
      }
      boolean updated = refToMirror.replace(ref, old, merged);
      if (updated) {
        return merged;
      }
    }
  }

  private static final boolean PRE_PARSE_PROPERTIES = false;

  /**
   * Looks up {@link ValueMirror} in map, loads them if needed or reloads them
   * if property data is unavailable (or expired).
   */
  @Override
  public SubpropertiesMirror getOrLoadSubproperties(Long ref) throws MethodIsBlockingException {
    ValueMirror mirror = getSafe(refToMirror, ref);

    SubpropertiesMirror references;
    if (mirror == null) {
      references = null;
    } else {
      references = mirror.getProperties();
    }
    if (references == null) {
      // need to look up this value again
      List<ValueMirror> loadedMirrors =
          loadValuesFromRemote(Collections.singletonList(ref));
      ValueMirror loadedMirror = loadedMirrors.get(0);
      references = loadedMirror.getProperties();
      if (references == null) {
        throw new RuntimeException("Failed to load properties");
      }
    }
    return references;
  }

  /**
   * Looks up data for scope on remote in form of scope object handle.
   */
  public ObjectValueHandle loadScopeFields(int scopeNumber,
      DebuggerMessageFactory.ScopeHostParameter host) throws MethodIsBlockingException {
    DebuggerMessage message = DebuggerMessageFactory.scope(scopeNumber, host);

    V8BlockingCallback<ObjectValueHandle> callback = new V8BlockingCallback<ObjectValueHandle>() {
      @Override
      protected ObjectValueHandle handleSuccessfulResponse(
          SuccessCommandResponse response) {
        return readFromScopeResponse(response);
      }
    };

    try {
      return V8Helper.callV8Sync(context, message, callback);
    } catch (ContextDismissedCheckedException e) {
      context.getDebugSession().maybeRethrowContextException(e);
      // or
      return null;
    }
  }

  private ObjectValueHandle readFromScopeResponse(SuccessCommandResponse response) {
    List<SomeHandle> refs = response.refs();

    for (SomeHandle handle : refs) {
      addHandleFromRefs(handle);
    }

    ScopeBody body;
    try {
      body = response.body().asScopeBody();
    } catch (JsonProtocolParseException e) {
      throw new ValueLoadException(e);
    }
    return body.object();
  }

  /**
   * For each PropertyReference from propertyRefs tries to either: 1. read it from PropertyReference
   * (possibly cached value) or 2. lookup value by refId from remote
   */
  @Override
  public List<ValueMirror> getOrLoadValueFromRefs(List<? extends PropertyReference> propertyRefs)
      throws MethodIsBlockingException {
    ValueMirror[] result = new ValueMirror[propertyRefs.size()];
    Map<Long, Integer> refToRequestIndex = new HashMap<Long, Integer>();
    List<PropertyReference> needsLoading = new ArrayList<PropertyReference>();

    for (int i = 0; i < propertyRefs.size(); i++) {
      PropertyReference property = propertyRefs.get(i);
      DataWithRef dataWithRef = property.getValueObject();
      Long ref = dataWithRef.ref();
      RefWithDisplayData dataWithDisplayData = dataWithRef.getWithDisplayData();
      ValueMirror mirror;
      if (dataWithDisplayData == null) {
        mirror = getSafe(refToMirror, ref);
      } else {
        mirror = ValueMirror.create(dataWithDisplayData, loadableStringFactory);
      }
      if (mirror == null) {
        // We don't have the data (enough) right now. We are requesting them from server.
        // There might be simultaneous request for the same value, which is a normal though
        // undesired case.
        Integer requestPos = getSafe(refToRequestIndex, ref);
        if (requestPos == null) {
          refToRequestIndex.put(ref, needsLoading.size());
          needsLoading.add(property);
        }
      } else {
        result[i] = mirror;
      }
    }

    if (!needsLoading.isEmpty()) {
      List<Long> refIds = getRefIdFromReferences(needsLoading);
      List<ValueMirror> loadedMirrors = loadValuesFromRemote(refIds);
      assert refIds.size() == loadedMirrors.size();
      for (int i = 0; i < propertyRefs.size(); i++) {
        if (result[i] == null) {
          PropertyReference property = propertyRefs.get(i);
          DataWithRef dataWithRef = property.getValueObject();
          Long ref = dataWithRef.ref();
          int pos = getSafe(refToRequestIndex, ref);
          result[i] = loadedMirrors.get(pos);
        }
      }
    }
    return Arrays.asList(result);
  }

  private static List<Long> getRefIdFromReferences(final List<PropertyReference> propertyRefs) {
    List<Long> result = new ArrayList<Long>(propertyRefs.size());
    for (PropertyReference ref : propertyRefs) {
      result.add(Long.valueOf(ref.getRef()));
    }
    return result;
  }

  /**
   * Requests values from remote via "lookup" command. Automatically caches received data.
   * @param propertyRefIds list of ref ids we need to look up
   * @return loaded value mirrors in the same order as in propertyRefIds
   */
  public List<ValueMirror> loadValuesFromRemote(final List<Long> propertyRefIds)
      throws MethodIsBlockingException {
    if (propertyRefIds.isEmpty()) {
      return Collections.emptyList();
    }

    DebuggerMessage message = DebuggerMessageFactory.lookup(propertyRefIds, false);

    V8BlockingCallback<List<ValueMirror>> callback =
        new V8BlockingCallback<List<ValueMirror>>() {
      @Override
      protected List<ValueMirror> handleSuccessfulResponse(
          SuccessCommandResponse response) {
        return readResponseFromLookup(response, propertyRefIds);
      }
    };

    try {
      return V8Helper.callV8Sync(context, message, callback);
    } catch (ContextDismissedCheckedException e) {
      context.getDebugSession().maybeRethrowContextException(e);
      // or
      throw new ValueLoadException("Invalid context", e);
    }
  }

  private List<ValueMirror> readResponseFromLookup(
      SuccessCommandResponse successResponse, List<Long> propertyRefIds) {
    List<ValueMirror> result = new ArrayList<ValueMirror>(propertyRefIds.size());
    JSONObject body;
    try {
      body = successResponse.body().asLookupMap();
    } catch (JsonProtocolParseException e) {
      throw new ValueLoadException(e);
    }
    for (int i = 0; i < propertyRefIds.size(); i++) {
      int ref = propertyRefIds.get(i).intValue();
      JSONObject value = JsonUtil.getAsJSON(body, String.valueOf(ref));
      if (value == null) {
        throw new ValueLoadException("Failed to find value for ref=" + ref);
      }
      ValueHandle valueHandle;
      try {
        valueHandle = V8ProtocolParserAccess.get().parseValueHandle(value);
      } catch (JsonProtocolParseException e) {
        throw new RuntimeException(e);
      }

      long refLong = valueHandle.handle();
      if (refLong != ref) {
        throw new ValueLoadException("Inconsistent ref in response, ref=" + ref);
      }
      ValueMirror mirror = addDataToMap(valueHandle);
      result.add(mirror);
    }
    return result;
  }

  private List<ValueHandle> readResponseFromLookupRaw(SuccessCommandResponse successResponse,
      List<Long> propertyRefIds) {
    List<ValueHandle> result = new ArrayList<ValueHandle>(propertyRefIds.size());
    JSONObject body;
    try {
      body = successResponse.body().asLookupMap();
    } catch (JsonProtocolParseException e) {
      throw new ValueLoadException(e);
    }
    for (int i = 0; i < propertyRefIds.size(); i++) {
      int ref = propertyRefIds.get(i).intValue();
      JSONObject value = JsonUtil.getAsJSON(body, String.valueOf(ref));
      if (value == null) {
        throw new ValueLoadException("Failed to find value for ref=" + ref);
      }
      ValueHandle valueHandle;
      try {
        valueHandle = V8ProtocolParserAccess.get().parseValueHandle(value);
      } catch (JsonProtocolParseException e) {
        throw new ValueLoadException(e);
      }

      addDataToMap(valueHandle);

      result.add(valueHandle);
    }
    return result;
  }

  private RelayOk relookupValue(long handleId, Long maxLength,
      final GenericCallback<ValueHandle> callback,
      SyncCallback syncCallback) throws ContextDismissedCheckedException {
    final List<Long> ids = Collections.singletonList(handleId);
    DebuggerMessage message = new LookupMessage(ids, false, maxLength);

    V8CommandCallbackBase innerCallback = new V8CommandCallbackBase() {
      @Override
      public void success(SuccessCommandResponse successResponse) {
        List<ValueHandle> handleList = readResponseFromLookupRaw(successResponse, ids);
        callback.success(handleList.get(0));
      }
      @Override
      public void failure(String message) {
        callback.failure(new Exception(message));
      }
    };

    return this.context.sendV8CommandAsync(message, true, innerCallback, syncCallback);
  }

  private class StringFactory implements LoadableString.Factory {
    @Override
    public LoadableString create(ValueHandle handle) {
      final long handleId = handle.handle();
      final LoadedValue initialValue = new LoadedValue(handle);

      return new LoadableString() {
        private final AtomicReference<LoadedValue> valueRef =
            new AtomicReference<LoadedValue>(initialValue);

        @Override
        public String getCurrentString() {
          return valueRef.get().stringValue;
        }

        @Override
        public boolean needsReload() {
          LoadedValue loadedValue = valueRef.get();
          return loadedValue.loadedSize < loadedValue.actualSize;
        }

        @Override
        public RelayOk reloadBigger(final GenericCallback<Void> callback,
            SyncCallback syncCallback) {

          long currentlyLoadedSize = valueRef.get().loadedSize;
          long newRequstedSize = chooseNewMaxStringLength(currentlyLoadedSize);

          GenericCallback<ValueHandle> innerCallback =
              new GenericCallback<ValueHandle>() {
            @Override
            public void success(ValueHandle handle) {
              LoadedValue newLoadedValue = new LoadedValue(handle);
              replaceValue(handle, newLoadedValue);
              if (callback != null) {
                callback.success(null);
              }
            }

            @Override
            public void failure(Exception e) {
              if (callback != null) {
                callback.failure(new Exception(e));
              }
            }
          };

          try {
            return relookupValue(handleId, newRequstedSize, innerCallback, syncCallback);
          } catch (final ContextDismissedCheckedException e) {
            DebugSession debugSession = context.getDebugSession();
            debugSession.maybeRethrowContextException(e);
            // or
            return debugSession.sendLoopbackMessage(new Runnable() {
              @Override
              public void run() {
                if (callback != null) {
                  callback.failure(e);
                }
              }
            }, syncCallback);
          }
        }

        private void replaceValue(ValueHandle handle, LoadedValue newValue) {
          while (true) {
            LoadedValue currentValue = valueRef.get();
            if (currentValue.loadedSize >= newValue.loadedSize) {
              return;
            }
            boolean updated = valueRef.compareAndSet(currentValue, newValue);
            if (updated) {
              return;
            }
          }
        }
        private long chooseNewMaxStringLength(long currentSize) {
          return Math.max(currentSize * 10, 64 * 1024);
        }
      };
    }

    private class LoadedValue {
      final String stringValue;
      final long loadedSize;
      final long actualSize;

      LoadedValue(ValueHandle handle) {
        this.stringValue = (String) handle.value();
        Long toIndex = handle.toIndex();
        if (toIndex == null) {
          this.loadedSize = this.stringValue.length();
          this.actualSize = this.loadedSize;
        } else {
          this.loadedSize = toIndex;
          this.actualSize = handle.length();
        }
      }
    }
  }
}
