// 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.wip;

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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.chromium.sdk.Breakpoint;
import org.chromium.sdk.CallFrame;
import org.chromium.sdk.DebugContext;
import org.chromium.sdk.DebugContext.StepAction;
import org.chromium.sdk.ExceptionData;
import org.chromium.sdk.JavascriptVm;
import org.chromium.sdk.JsArray;
import org.chromium.sdk.JsEvaluateContext;
import org.chromium.sdk.JsFunction;
import org.chromium.sdk.JsObject;
import org.chromium.sdk.JsScope;
import org.chromium.sdk.JsValue;
import org.chromium.sdk.JsVariable;
import org.chromium.sdk.RelayOk;
import org.chromium.sdk.RemoteValueMapping;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.TextStreamPosition;
import org.chromium.sdk.internal.protocolparser.JsonProtocolParseException;
import org.chromium.sdk.internal.wip.WipExpressionBuilder.ValueNameBuilder;
import org.chromium.sdk.internal.wip.WipValueLoader.Getter;
import org.chromium.sdk.internal.wip.protocol.WipParserAccess;
import org.chromium.sdk.internal.wip.protocol.input.WipCommandResponse;
import org.chromium.sdk.internal.wip.protocol.input.debugger.CallFrameValue;
import org.chromium.sdk.internal.wip.protocol.input.debugger.EvaluateOnCallFrameData;
import org.chromium.sdk.internal.wip.protocol.input.debugger.PausedEventData;
import org.chromium.sdk.internal.wip.protocol.input.debugger.ResumedEventData;
import org.chromium.sdk.internal.wip.protocol.input.debugger.ScopeValue;
import org.chromium.sdk.internal.wip.protocol.input.runtime.EvaluateData;
import org.chromium.sdk.internal.wip.protocol.input.runtime.PropertyDescriptorValue;
import org.chromium.sdk.internal.wip.protocol.input.runtime.RemoteObjectValue;
import org.chromium.sdk.internal.wip.protocol.output.WipParams;
import org.chromium.sdk.internal.wip.protocol.output.WipParamsWithResponse;
import org.chromium.sdk.internal.wip.protocol.output.debugger.EvaluateOnCallFrameParams;
import org.chromium.sdk.internal.wip.protocol.output.debugger.ResumeParams;
import org.chromium.sdk.internal.wip.protocol.output.debugger.StepIntoParams;
import org.chromium.sdk.internal.wip.protocol.output.debugger.StepOutParams;
import org.chromium.sdk.internal.wip.protocol.output.debugger.StepOverParams;
import org.chromium.sdk.internal.wip.protocol.output.runtime.EvaluateParams;
import org.chromium.sdk.util.AsyncFutureRef;
import org.chromium.sdk.util.GenericCallback;
import org.chromium.sdk.util.LazyConstructable;
import org.chromium.sdk.util.MethodIsBlockingException;
import org.chromium.sdk.util.RelaySyncCallback;
import org.json.simple.JSONObject;

/**
 * Builder for {@link DebugContext} that works with Wip protocol.
 */
class WipContextBuilder {
  private static final Logger LOGGER = Logger.getLogger(WipContextBuilder.class.getName());

  private final WipTabImpl tabImpl;
  private final EvaluateHack evaluateHack;
  private WipDebugContextImpl currentContext = null;

  WipContextBuilder(WipTabImpl tabImpl) {
    this.tabImpl = tabImpl;
    this.evaluateHack = new EvaluateHack(tabImpl);
  }

  // Called from Dispatch Thread.
  RelayOk updateStackTrace(List<CallFrameValue> callFrames,
      GenericCallback<Void> callback, final SyncCallback syncCallback) {
    if (currentContext == null) {
      if (callback != null) {
        callback.success(null);
      }
      return RelaySyncCallback.finish(syncCallback);
    } else {
      return currentContext.setFrames(callFrames, callback, syncCallback);
    }
  }

  void createContext(PausedEventData data) {
    if (currentContext != null) {
      LOGGER.severe("Context is already created");
      currentContext = null;
    }

    final WipDebugContextImpl context = new WipDebugContextImpl(data);
    currentContext = context;

    GenericCallback<Void> callback = new GenericCallback<Void>() {
      @Override
      public void success(Void value) {
        tabImpl.getDebugListener().getDebugEventListener().suspended(context);
      }

      @Override
      public void failure(Exception exception) {
        throw new RuntimeException(exception);
      }
    };

    context.setFrames(data.callFrames(), callback, null);
  }

  EvaluateHack getEvaluateHack() {
    return evaluateHack;
  }

  void onResumeReportedFromRemote(ResumedEventData event) {
    if (currentContext == null) {
      throw new IllegalStateException();
    }
    WipDebugContextImpl context = currentContext;
    currentContext = null;
    this.tabImpl.getDebugListener().getDebugEventListener().resumed();
    context.reportClosed();
  }

  class WipDebugContextImpl implements DebugContext {
    private volatile List<CallFrameImpl> frames = null;
    private final ExceptionData exceptionData;
    private final AtomicReference<CloseRequest> closeRequest =
        new AtomicReference<CloseRequest>(null);
    private final JsEvaluateContext globalContext;

    public WipDebugContextImpl(PausedEventData data) {
      PausedEventData.Data additionalData = data.data();
      if (data.reason() == PausedEventData.Reason.EXCEPTION && additionalData != null) {
        RemoteObjectValue exceptionRemoteObject;
        try {
          JSONObject additionalDataObject = additionalData.getUnderlyingObject();
          exceptionRemoteObject =
              WipParserAccess.get().parseRemoteObjectValue(additionalDataObject);
        } catch (JsonProtocolParseException e) {
          throw new RuntimeException("Failed to parse exception data", e);
        }
        JsValue exceptionValue =
            valueLoader.getValueBuilder().wrap(exceptionRemoteObject, null);
        exceptionData = new ExceptionDataImpl(exceptionValue);
      } else {
        exceptionData = null;
      }
      globalContext = new GlobalEvaluateContext(getValueLoader());
    }

    RelayOk setFrames(List<CallFrameValue> frameDataList,
        final GenericCallback<Void> callback, final SyncCallback syncCallback) {
      frames = new ArrayList<CallFrameImpl>(frameDataList.size());
      for (CallFrameValue frameData : frameDataList) {
        frames.add(new CallFrameImpl(frameData));
      }

      return tabImpl.getScriptManager().loadScriptSourcesAsync(getScriptIds(),
          new WipScriptManager.ScriptSourceLoadCallback() {
            @Override
            public void done(Map<String, WipScriptImpl> loadedScripts) {
              setScripts(loadedScripts);

              if (callback != null) {
                callback.success(null);
              }
            }
          },
          syncCallback);
    }

    void resetFrames(List<CallFrameValue> frameDataList) {
      List<CallFrameImpl> newFrames = new ArrayList<CallFrameImpl>(frameDataList.size());
      for (int i = 0; i < frameDataList.size(); i++) {
        CallFrameImpl callFrameImpl = new CallFrameImpl(frameDataList.get(i));
        callFrameImpl.setScript(frames.get(i).getScript());
        newFrames.add(callFrameImpl);
      }
      frames = newFrames;
    }

    WipValueLoader getValueLoader() {
      return valueLoader;
    }

    void reportClosed() {
      CloseRequest request = this.closeRequest.get();
      if (request != null && request.callback != null) {
        request.callback.success();
      }
    }

    Set<String> getScriptIds() {
      Set<String> scriptIds = new HashSet<String>();
      for (CallFrameImpl frame : frames) {
        String sourceId = frame.getSourceId();
        if (sourceId != null) {
          scriptIds.add(sourceId);
        }
      }
      return scriptIds;
    }

    void setScripts(Map<String, WipScriptImpl> loadedScripts) {
      for (CallFrameImpl frame : frames) {
        String sourceId = frame.getSourceId();
        if (sourceId != null) {
          WipScriptImpl script = getSafe(loadedScripts, sourceId);
          // Script can be null.
          frame.setScript(script);
        }
      }
    }

    @Override
    public State getState() {
      if (exceptionData == null) {
        return State.NORMAL;
      } else {
        return State.EXCEPTION;
      }
    }

    @Override
    public ExceptionData getExceptionData() {
      return exceptionData;
    }

    @Override
    public Collection<? extends Breakpoint> getBreakpointsHit() {
      if (frames.isEmpty()) {
        return Collections.emptyList();
      }
      CallFrameImpl topFrame = frames.get(0);
      return tabImpl.getBreakpointManager().findRelatedBreakpoints(topFrame);
    }

    @Override
    public List<? extends CallFrame> getCallFrames() {
      return frames;
    }

    @Override
    public void continueVm(StepAction stepAction, int stepCount,
        ContinueCallback callback) {
      continueVm(stepAction, stepCount, callback, null);
    }

    @Override
    public RelayOk continueVm(StepAction stepAction, int stepCount,
        final ContinueCallback callback, SyncCallback syncCallback) {
      {
        boolean updated = closeRequest.compareAndSet(null, new CloseRequest(callback));
        if (!updated) {
          throw new IllegalStateException("Continue already requested");
        }
      }

      WipParams params = sdkStepToProtocolStep(stepAction);

      WipCommandCallback commandCallback;
      if (callback == null) {
        commandCallback = null;
      } else {
        commandCallback = new WipCommandCallback() {
          @Override public void messageReceived(WipCommandResponse response) {
            callback.success();
          }
          @Override public void failure(String message) {
            callback.failure(message);
          }
        };
      }
      return tabImpl.getCommandProcessor().send(params, commandCallback, syncCallback);
    }

    @Override
    public JsEvaluateContext getGlobalEvaluateContext() {
      return globalContext;
    }

    @Override
    public RemoteValueMapping getDefaultRemoteValueMapping() {
      return valueLoader;
    }

    public WipTabImpl getTab() {
      return tabImpl;
    }

    public WipCommandProcessor getCommandProcessor() {
      return tabImpl.getCommandProcessor();
    }

    private class CloseRequest {
      final ContinueCallback callback;

      CloseRequest(ContinueCallback callback) {
        this.callback = callback;
      }
    }

    class CallFrameImpl implements CallFrame {
      private final String functionName;
      private final String id;
      private final LazyConstructable<List<JsScope>> scopeData;
      private final JsVariable thisObject;
      private final TextStreamPosition streamPosition;
      private final String sourceId;
      private WipScriptImpl scriptImpl;

      public CallFrameImpl(CallFrameValue frameData) {
        functionName = frameData.functionName();
        id = frameData.callFrameId();
        sourceId = frameData.location().scriptId();
        final List<ScopeValue> scopeDataList = frameData.scopeChain();

        scopeData = LazyConstructable.create(new LazyConstructable.Factory<List<JsScope>>() {
          @Override
          public List<JsScope> construct() {
            final List<JsScope> scopes = new ArrayList<JsScope>(scopeDataList.size());

            for (int i = 0; i < scopeDataList.size(); i++) {
              ScopeValue scopeData = scopeDataList.get(i);
              scopes.add(createScope(scopeData, valueLoader));
            }
            return scopes;
          }
        });

        RemoteObjectValue thisObjectData = frameData.getThis();
        if (thisObjectData == null) {
          LOGGER.log(Level.SEVERE, "Missing local scope", new Exception());
          thisObject = null;
        } else {
          thisObject = createSimpleNameVariable("this", thisObjectData);
        }

        // 0-based.
        final int line = (int) frameData.location().lineNumber();

        // 0-based.
        // TODO: check documentation, whether it's 0-based
        Long columnObject = frameData.location().columnNumber();
        final int column;
        if (columnObject == null) {
          column = 0;
        } else {
          column = columnObject.intValue();
        }
        streamPosition = new TextStreamPosition() {
          @Override public int getOffset() {
            return WipBrowserImpl.throwUnsupported();
          }
          @Override public int getLine() {
            return line;
          }

          @Override public int getColumn() {
            return column;
          }
        };
      }

      String getSourceId() {
        return sourceId;
      }

      void setScript(WipScriptImpl scriptImpl) {
        this.scriptImpl = scriptImpl;
      }

      @Override
      public List<? extends JsScope> getVariableScopes() {
        return scopeData.get();
      }

      @Override
      public JsVariable getReceiverVariable() {
        return thisObject;
      }

      @Override
      public WipScriptImpl getScript() {
        return scriptImpl;
      }

      @Override
      public TextStreamPosition getStatementStartPosition() {
        return streamPosition;
      }

      @Override
      public String getFunctionName() {
        return functionName;
      }

      @Override
      public JsEvaluateContext getEvaluateContext() {
        return evaluateContext;
      }

      private JsVariable createSimpleNameVariable(final String name,
          RemoteObjectValue thisObjectData) {
        ValueNameBuilder valueNameBuidler = WipExpressionBuilder.createRootName(name, false);
        return valueLoader.getValueBuilder().createVariable(thisObjectData, valueNameBuidler);
      }

      private final WipEvaluateContextBase<?> evaluateContext =
          new WipEvaluateContextBase<EvaluateOnCallFrameData>(getValueLoader()) {
        @Override
        protected WipParamsWithResponse<EvaluateOnCallFrameData> createRequestParams(
            String expression, WipValueLoader destinationValueLoader) {
          return new EvaluateOnCallFrameParams(id, expression,
              destinationValueLoader.getObjectGroupId(), false, null, false);
        }

        @Override protected RemoteObjectValue getRemoteObjectValue(EvaluateOnCallFrameData data) {
          return data.result();
        }

        @Override protected Boolean getWasThrown(EvaluateOnCallFrameData data) {
          return data.wasThrown();
        }
      };
    }

    private class ExceptionDataImpl implements ExceptionData {
      private final JsValue exceptionValue;

      ExceptionDataImpl(JsValue exceptionValue) {
        this.exceptionValue = exceptionValue;
      }

      @Override
      public JsValue getExceptionValue() {
        return exceptionValue;
      }

      @Override
      public boolean isUncaught() {
        // TODO: implement.
        return false;
      }

      @Override
      public String getSourceText() {
        // Not supported.
        return null;
      }

      @Override
      public String getExceptionMessage() {
        // TODO: implement.
        return exceptionValue.getValueString();
      }
    }

    @Override
    public JavascriptVm getJavascriptVm() {
      return tabImpl;
    }

    private final WipValueLoader valueLoader = new WipValueLoader(tabImpl) {
      @Override
      String getObjectGroupId() {
        return null;
      }
    };
  }

  static JsScope createScope(ScopeValue scopeData, WipValueLoader valueLoader) {
    JsScope.Type type = WIP_TO_SDK_SCOPE_TYPE.get(scopeData.type());
    if (type == null) {
      type = JsScope.Type.UNKNOWN;
    }
    if (type == JsScope.Type.WITH) {
      return new WithScopeImpl(scopeData, valueLoader);
    } else {
      return new ScopeImpl(scopeData, type, valueLoader);
    }
  }

  private static class ScopeImpl implements JsScope {
    private final AsyncFutureRef<Getter<ScopeVariables>> propertiesRef =
        new AsyncFutureRef<Getter<ScopeVariables>>();
    private final String objectId;
    private final Type type;
    private final WipValueLoader valueLoader;

    public ScopeImpl(ScopeValue scopeData, Type type, WipValueLoader valueLoader) {
      this.type = type;
      this.objectId = scopeData.object().objectId();
      this.valueLoader = valueLoader;
    }

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

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

    @Override
    public List<? extends JsVariable> getVariables() throws MethodIsBlockingException {
      int currentCacheState = valueLoader.getCacheState();
      if (propertiesRef.isInitialized()) {
        ScopeVariables result = propertiesRef.getSync().get();
        if (result.cacheState == currentCacheState) {
          return result.variables;
        }
        startLoadOperation(true, currentCacheState);
      } else {
        startLoadOperation(false, currentCacheState);
      }

      // This is blocking.
      return propertiesRef.getSync().get().variables;
    }

    /**
     * Starts load operation that works synchronously, i.e. it may block the calling method.
     * This is done because some thread must take post-processing anyway and it shouldn't
     * be the Dispatch thread.
     * The method may not be blocking, if another thread is already doing the same operation.
     */
    private void startLoadOperation(boolean reload, int currentCacheState)
        throws MethodIsBlockingException {
      WipValueLoader.LoadPostprocessor<Getter<ScopeVariables>> processor =
          new WipValueLoader.LoadPostprocessor<Getter<ScopeVariables>>() {
        @Override
        public Getter<ScopeVariables> process(
            List<? extends PropertyDescriptorValue> propertyList, int currentCacheState) {
          final List<JsVariable> properties = new ArrayList<JsVariable>(propertyList.size());

          WipValueBuilder valueBuilder = valueLoader.getValueBuilder();
          for (PropertyDescriptorValue property : propertyList) {
            final String name = property.name();

            ValueNameBuilder valueNameBuilder =
                WipExpressionBuilder.createRootName(name, false);

            JsVariable variable;
            if (objectId == null) {
              variable = valueBuilder.createVariable(property.value(), valueNameBuilder);
            } else {
              variable = valueBuilder.createObjectProperty(property, objectId, valueNameBuilder);
            }
            properties.add(variable);
          }
          final ScopeVariables scopeVariables = new ScopeVariables(properties, currentCacheState);
          return new Getter<ScopeVariables>() {
            @Override
            ScopeVariables get() {
              return scopeVariables;
            }
          };
        }

        @Override
        public Getter<ScopeVariables> getEmptyResult() {
          return EMPTY_SCOPE_VARIABLES_OPTIONAL;
        }

        @Override
        public Getter<ScopeVariables> forException(Exception exception) {
          return WipValueLoader.Getter.newFailure(exception);
        }
      };
      // This is blocking.
      valueLoader.loadPropertiesInFuture(objectId, processor, reload, currentCacheState,
          propertiesRef);
    }
  }

  private static class WithScopeImpl implements JsScope.WithScope {
    private final JsValue jsValue;

    WithScopeImpl(ScopeValue scopeData, WipValueLoader valueLoader) {
      jsValue = valueLoader.getValueBuilder().wrap(scopeData.object(), null);
    }

    @Override
    public Type getType() {
      return Type.WITH;
    }

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

    @Override
    public List<? extends JsVariable> getVariables() throws MethodIsBlockingException {
      JsObject asObject = jsValue.asObject();
      if (asObject == null) {
        return Collections.emptyList();
      }
      return new ArrayList<JsVariable>(asObject.getProperties());
    }

    @Override
    public JsValue getWithArgument() {
      return jsValue;
    }
  }

  static JsVariable wrapExceptionValue(RemoteObjectValue valueData,
      WipValueBuilder valueBuilder) {
    JsValue exceptionValue = valueBuilder.wrap(valueData, null);

    final JsVariable property =
        WipValueBuilder.createVariable(exceptionValue, EVALUATE_EXCEPTION_INNER_NAME);

    JsObject wrapperValue = new JsObject() {
      @Override
      public RelayOk reloadHeavyValue(ReloadBiggerCallback callback,
          SyncCallback syncCallback) {
        throw new UnsupportedOperationException();
      }

      @Override
      public boolean isTruncated() {
        return false;
      }

      @Override
      public String getValueString() {
        return "<abnormal return>";
      }

      @Override
      public Type getType() {
        return Type.TYPE_OBJECT;
      }

      @Override
      public JsObject asObject() {
        return this;
      }

      @Override
      public String getRefId() {
        return null;
      }

      @Override
      public RemoteValueMapping getRemoteValueMapping() {
        return null;
      }

      @Override
      public JsVariable getProperty(String name) {
        if (name.equals(property.getName())) {
          return property;
        }
        return null;
      }

      @Override
      public Collection<? extends JsVariable> getProperties()
          throws MethodIsBlockingException {
        return Collections.singletonList(property);
      }

      @Override
      public Collection<? extends JsVariable> getInternalProperties()
          throws MethodIsBlockingException {
        return Collections.emptyList();
      }

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

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

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

    return WipValueBuilder.createVariable(wrapperValue, EVALUATE_EXCEPTION_NAME);
  }

  static final class GlobalEvaluateContext extends WipEvaluateContextBase<EvaluateData> {

    GlobalEvaluateContext(WipValueLoader valueLoader) {
      super(valueLoader);
    }

    @Override protected WipParamsWithResponse<EvaluateData> createRequestParams(String expression,
        WipValueLoader destinationValueLoader) {
      boolean doNotPauseOnExceptions = true;
      return new EvaluateParams(expression, destinationValueLoader.getObjectGroupId(),
          false, doNotPauseOnExceptions, null, false);
    }

    @Override protected RemoteObjectValue getRemoteObjectValue(EvaluateData data) {
      return data.result();
    }

    @Override protected Boolean getWasThrown(EvaluateData data) {
      return data.wasThrown();
    }
  }

  private static final Map<ScopeValue.Type, JsScope.Type> WIP_TO_SDK_SCOPE_TYPE;
  static {
    WIP_TO_SDK_SCOPE_TYPE = new HashMap<ScopeValue.Type, JsScope.Type>();

    WIP_TO_SDK_SCOPE_TYPE.put(ScopeValue.Type.GLOBAL, JsScope.Type.GLOBAL);
    WIP_TO_SDK_SCOPE_TYPE.put(ScopeValue.Type.LOCAL, JsScope.Type.LOCAL);
    WIP_TO_SDK_SCOPE_TYPE.put(ScopeValue.Type.WITH, JsScope.Type.WITH);
    WIP_TO_SDK_SCOPE_TYPE.put(ScopeValue.Type.CLOSURE, JsScope.Type.CLOSURE);
    WIP_TO_SDK_SCOPE_TYPE.put(ScopeValue.Type.CATCH, JsScope.Type.CATCH);

    assert WIP_TO_SDK_SCOPE_TYPE.size() == ScopeValue.Type.values().length;
  }

  private static final ValueNameBuilder EXCEPTION_NAME =
      WipExpressionBuilder.createRootNameNoDerived("exception");

  private static final ValueNameBuilder WITH_OBJECT_NAME =
      WipExpressionBuilder.createRootNameNoDerived("<with object>");

  private static final ValueNameBuilder EVALUATE_EXCEPTION_INNER_NAME =
      WipExpressionBuilder.createRootNameNoDerived("<exception>");

  private static final ValueNameBuilder EVALUATE_EXCEPTION_NAME =
      WipExpressionBuilder.createRootNameNoDerived("<thrown exception>");

  private static class ScopeVariables {
    final List<JsVariable> variables;
    final int cacheState;

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

  private static final Getter<ScopeVariables> EMPTY_SCOPE_VARIABLES_OPTIONAL =
      new Getter<ScopeVariables>() {
        private final ScopeVariables value =
          new ScopeVariables(Collections.<JsVariable>emptyList(), Integer.MAX_VALUE);

        @Override ScopeVariables get() {
          return value;
        }
      };


  private WipParams sdkStepToProtocolStep(StepAction stepAction) {
    switch (stepAction) {
    case CONTINUE:
      return RESUME_PARAMS;
    case IN:
      return STEP_INTO_PARAMS;
    case OUT:
      return STEP_OUT_PARAMS;
    case OVER:
      return STEP_OVER_PARAMS;
    default:
      throw new RuntimeException();
    }
  }

  private static final ResumeParams RESUME_PARAMS = new ResumeParams();
  private static final StepIntoParams STEP_INTO_PARAMS = new StepIntoParams();
  private static final StepOutParams STEP_OUT_PARAMS = new StepOutParams();
  private static final StepOverParams STEP_OVER_PARAMS = new StepOverParams();
}