// 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 java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.chromium.sdk.Breakpoint;
import org.chromium.sdk.BreakpointTypeExtension;
import org.chromium.sdk.IgnoreCountBreakpointExtension;
import org.chromium.sdk.JavascriptVm.BreakpointCallback;
import org.chromium.sdk.RelayOk;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.internal.ScriptRegExpBreakpointTarget;
import org.chromium.sdk.internal.wip.protocol.input.WipCommandResponse.Success;
import org.chromium.sdk.internal.wip.protocol.input.debugger.LocationValue;
import org.chromium.sdk.internal.wip.protocol.input.debugger.SetBreakpointByUrlData;
import org.chromium.sdk.internal.wip.protocol.input.debugger.SetBreakpointData;
import org.chromium.sdk.internal.wip.protocol.output.WipParamsWithResponse;
import org.chromium.sdk.internal.wip.protocol.output.debugger.LocationParam;
import org.chromium.sdk.internal.wip.protocol.output.debugger.RemoveBreakpointParams;
import org.chromium.sdk.internal.wip.protocol.output.debugger.SetBreakpointByUrlParams;
import org.chromium.sdk.internal.wip.protocol.output.debugger.SetBreakpointParams;
import org.chromium.sdk.util.GenericCallback;
import org.chromium.sdk.util.RelaySyncCallback;

/**
 * Wip-based breakpoint implementation.
 * The implementation is based on volatile fields and expects client code to do some
 * synchronization (serialize calls to setters, {@link #flush} and {@link #clear}).
 */
public class WipBreakpointImpl implements Breakpoint {
  private final WipBreakpointManager breakpointManager;

  private final Target target;

  private final int lineNumber;
  private final int columnNumber;

  private final int sdkId;
  private volatile String protocolId = null;

  private volatile String condition;
  private volatile boolean enabled;
  private volatile boolean isDirty;

  // Access only from Dispatch thread.
  private Set<ActualLocation> actualLocations = new HashSet<ActualLocation>(2);

  public WipBreakpointImpl(WipBreakpointManager breakpointManager, int sdkId, Target target,
      int lineNumber, int columnNumber, String condition, boolean enabled) {
    this.breakpointManager = breakpointManager;
    this.sdkId = sdkId;
    this.target = target;
    this.lineNumber = lineNumber;
    this.columnNumber = columnNumber;
    this.condition = condition;
    this.enabled = enabled;

    this.isDirty = false;
  }

  @Override
  public Target getTarget() {
    return target;
  }

  @Override
  public long getId() {
    return sdkId;
  }

  public static final BreakpointTypeExtension TYPE_EXTENSION = new BreakpointTypeExtension() {
    @Override
    public FunctionSupport getFunctionSupport() {
      return null;
    }
    @Override
    public ScriptRegExpSupport getScriptRegExpSupport() {
      return scriptRegExpSupport;
    }

    private final ScriptRegExpSupport scriptRegExpSupport = new ScriptRegExpSupport() {
      @Override
      public Target createTarget(String regExp) {
        return new ScriptRegExpBreakpointTarget(regExp);
      }
    };
  };

  @Override
  public long getLineNumber() {
    return lineNumber;
  }

  @Override
  public boolean isEnabled() {
    return enabled;
  }

  @Override
  public void setEnabled(boolean enabled) {
    if (enabled == this.enabled) {
      return;
    }
    this.enabled = enabled;
    isDirty  = true;
  }

  @Override
  public String getCondition() {
    return condition;
  }

  @Override
  public void setCondition(String condition) {
    if (eq(this.condition, condition)) {
      return;
    }
    this.condition = condition;
    isDirty = true;
  }

  @Override
  public IgnoreCountBreakpointExtension getIgnoreCountBreakpointExtension() {
    return getIgnoreCountBreakpointExtensionImpl();
  }

  public static IgnoreCountBreakpointExtension getIgnoreCountBreakpointExtensionImpl() {
    // TODO(peter.rybin): implement when protocol supports.
    return null;
  }


  void setRemoteData(String protocolId, Collection<ActualLocation> actualLocations) {
    this.protocolId = protocolId;
    this.actualLocations.clear();
    this.actualLocations.addAll(actualLocations);
    this.breakpointManager.getDb().setIdMapping(this, protocolId);
  }

  void addResolvedLocation(LocationValue locationValue) {
    ActualLocation location = locationFromProtocol(locationValue);
    actualLocations.add(location);
  }

  Set<ActualLocation> getActualLocations() {
    return actualLocations;
  }

  void clearActualLocations() {
    actualLocations.clear();
  }

  void deleteSelfFromDb() {
    if (protocolId != null) {
      breakpointManager.getDb().setIdMapping(this, null);
    }
    breakpointManager.getDb().removeBreakpoint(this);
  }

  @Override
  public RelayOk clear(final BreakpointCallback callback, SyncCallback syncCallback) {
    // TODO: make sure this is thread-safe.
    if (protocolId == null) {
      breakpointManager.getDb().removeBreakpoint(this);
      callback.success(this);
      return RelaySyncCallback.finish(syncCallback);
    }

    RemoveBreakpointParams params = new RemoveBreakpointParams(protocolId);

    WipCommandCallback commandCallback;
    if (callback == null) {
      commandCallback = null;
    } else {
      commandCallback = new WipCommandCallback.Default() {
        @Override protected void onSuccess(Success success) {
          breakpointManager.getDb().setIdMapping(WipBreakpointImpl.this, null);
          breakpointManager.getDb().removeBreakpoint(WipBreakpointImpl.this);
          callback.success(WipBreakpointImpl.this);
        }
        @Override protected void onError(String message) {
          callback.failure(message);
        }
      };
    }

    return breakpointManager.getCommandProcessor().send(params, commandCallback, syncCallback);
  }

  @Override
  public RelayOk flush(final BreakpointCallback callback, final SyncCallback syncCallback) {
    final RelaySyncCallback relay = new RelaySyncCallback(syncCallback);

    if (!isDirty) {
      if (callback != null) {
        callback.success(this);
      }
      return RelaySyncCallback.finish(syncCallback);
    }

    isDirty = false;

    if (protocolId == null) {
      // Breakpoint was disabled, it doesn't exist in VM, immediately start step 2.
      return recreateBreakpointAsync(callback, relay);
    } else {
      // Call syncCallback if something goes wrong after we sent request.
      final RelaySyncCallback.Guard guard = relay.newGuard();

      WipCommandCallback removeCallback = new WipCommandCallback.Default() {
        @Override
        protected void onSuccess(Success success) {
          setRemoteData(null, Collections.<ActualLocation>emptyList());
          RelayOk relayOk = recreateBreakpointAsync(callback, relay);
          guard.discharge(relayOk);
        }

        @Override
        protected void onError(String message) {
          throw new RuntimeException("Failed to remove breakpoint: " + message);
        }
      };

      // Call syncCallback if something goes wrong.
      return breakpointManager.getCommandProcessor().send(new RemoveBreakpointParams(protocolId),
          removeCallback, guard.asSyncCallback());
    }
  }

  static class ActualLocation {
    private final String sourceId;
    private final long lineNumber;
    private final Long columnNumber;

    ActualLocation(String sourceId, long lineNumber, Long columnNumber) {
      this.sourceId = sourceId;
      this.lineNumber = lineNumber;
      this.columnNumber = columnNumber;
    }

    @Override
    public boolean equals(Object obj) {
      ActualLocation other = (ActualLocation) obj;
      return this.sourceId.equals(other.sourceId) &&
          this.lineNumber == other.lineNumber &&
          eq(this.columnNumber, other.columnNumber);
    }

    @Override
    public int hashCode() {
      int column;
      if (columnNumber == null) {
        column = 0;
      } else {
        column = columnNumber.intValue();
      }
      return sourceId.hashCode() + 31 * (int) lineNumber + column;
    }

    @Override
    public String toString() {
      return "<sourceId=" + sourceId + ", line=" + lineNumber + ", column=" + columnNumber + ">";
    }
  }

  private RelayOk recreateBreakpointAsync(final BreakpointCallback flushCallback,
      RelaySyncCallback relay) {

    if (enabled) {
      SetBreakpointCallback setCommandCallback = new SetBreakpointCallback() {
        @Override
        public void onSuccess(String protocolId, Collection<ActualLocation> actualLocations) {
          setRemoteData(protocolId, actualLocations);
          if (flushCallback != null) {
            flushCallback.success(WipBreakpointImpl.this);
          }
        }

        @Override
        public void onFailure(Exception exception) {
          if (flushCallback != null) {
            flushCallback.failure(exception.getMessage());
          }
        }
      };

      RelaySyncCallback.Guard guard = relay.newGuard();

      if (condition == null) {
        condition = "";
      }
      return sendSetBreakpointRequest(target, lineNumber, columnNumber, condition,
          setCommandCallback, guard.asSyncCallback(),
          breakpointManager.getCommandProcessor());
    } else {
      // Breakpoint is disabled, do not create it.
      RelayOk relayOk;
      try {
        if (flushCallback != null) {
          flushCallback.success(WipBreakpointImpl.this);
        }
      } finally {
        relayOk = relay.finish();
      }
      return relayOk;
    }
  }

  interface SetBreakpointCallback {
    void onSuccess(String breakpointId, Collection<ActualLocation> actualLocations);
    void onFailure(Exception cause);
  }

  /**
   * @param callback a generic callback that receives breakpoint protocol id
   * @return
   */
  static RelayOk sendSetBreakpointRequest(Target target, final int lineNumber,
      int columnNumber, final String condition,
      final SetBreakpointCallback callback, final SyncCallback syncCallback,
      final WipCommandProcessor commandProcessor) {

    final Long columnNumberParam;
    if (columnNumber == Breakpoint.EMPTY_VALUE) {
      columnNumberParam = null;
    } else {
      columnNumberParam = Long.valueOf(columnNumber);
    }

    return target.accept(new BreakpointTypeExtension.ScriptRegExpSupport.Visitor<RelayOk>() {
      @Override
      public RelayOk visitScriptName(String scriptName) {
        return sendRequest(scriptName, RequestHandler.FOR_URL);
      }

      @Override
      public RelayOk visitRegExp(String regExp) {
        return sendRequest(regExp, RequestHandler.FOR_REGEXP);
      }

      @Override
      public RelayOk visitScriptId(Object scriptId) {
        String scriptIdString = WipScriptManager.convertAlienSourceId(scriptId);
        return sendRequest(scriptIdString, RequestHandler.FOR_ID);
      }

      @Override
      public RelayOk visitUnknown(Target target) {
        throw new IllegalArgumentException();
      }

      private <T, DATA, PARAMS extends WipParamsWithResponse<DATA>> RelayOk sendRequest(
          T parameter, final RequestHandler<T, DATA, PARAMS> handler) {
        PARAMS requestParams =
            handler.createRequestParams(parameter, lineNumber, columnNumberParam, condition);

        GenericCallback<DATA> wrappedCallback;
        if (callback == null) {
          wrappedCallback = null;
        } else {
          wrappedCallback = new GenericCallback<DATA>() {
            @Override
            public void success(DATA data) {
              String breakpointId = handler.getBreakpointId(data);
              Collection<LocationValue> locationValues = handler.getActualLocations(data);
              List<ActualLocation> locationList =
                  new ArrayList<ActualLocation>(locationValues.size());
              for (LocationValue value : locationValues) {
                locationList.add(locationFromProtocol(value));
              }
              callback.onSuccess(breakpointId, locationList);
            }

            @Override
            public void failure(Exception exception) {
              callback.onFailure(exception);
            }
          };
        }

        return commandProcessor.send(requestParams, wrappedCallback, syncCallback);
      }
    });
  }

  private static abstract class RequestHandler<T,
      DATA, PARAMS extends WipParamsWithResponse<DATA>> {

    abstract PARAMS createRequestParams(T parameter, long lineNumber, Long columnNumberOpt,
        String conditionOpt);

    abstract String getBreakpointId(DATA data);

    abstract Collection<LocationValue> getActualLocations(DATA data);

    static abstract class ForUrlOrRegExp
        extends RequestHandler<String, SetBreakpointByUrlData, SetBreakpointByUrlParams> {
      @Override
      String getBreakpointId(SetBreakpointByUrlData data) {
        return data.breakpointId();
      }

      @Override
      Collection<LocationValue> getActualLocations(SetBreakpointByUrlData data) {
        return data.locations();
      }
    }

    static final ForUrlOrRegExp FOR_URL = new ForUrlOrRegExp() {
      @Override
      SetBreakpointByUrlParams createRequestParams(String url,
          long lineNumber, Long columnNumber, String condition) {
        return new SetBreakpointByUrlParams(lineNumber, url, null, columnNumber, condition);
      }
    };

    static final ForUrlOrRegExp FOR_REGEXP = new ForUrlOrRegExp() {
      @Override
      SetBreakpointByUrlParams createRequestParams(String url,
          long lineNumber, Long columnNumber, String condition) {
        return new SetBreakpointByUrlParams(lineNumber, null, url, columnNumber, condition);
      }
    };

    static final RequestHandler<String, SetBreakpointData, SetBreakpointParams> FOR_ID =
        new RequestHandler<String, SetBreakpointData, SetBreakpointParams>() {
          @Override
          SetBreakpointParams createRequestParams(String sourceId,
              long lineNumber, Long columnNumber, String condition) {
            LocationParam locationParam =
                new LocationParam(sourceId, lineNumber, columnNumber);
            return new SetBreakpointParams(locationParam, condition);
          }

          @Override
          String getBreakpointId(SetBreakpointData data) {
            return data.breakpointId();
          }

          @Override
          Collection<LocationValue> getActualLocations(SetBreakpointData data) {
            return Collections.singletonList(data.actualLocation());
          }
        };
  }

  private static ActualLocation locationFromProtocol(LocationValue locationValue) {
    return new ActualLocation(locationValue.scriptId(), locationValue.lineNumber(),
        locationValue.columnNumber());
  }

  private static <T> boolean eq(T left, T right) {
    return left == right || (left != null && left.equals(right));
  }
}
