// 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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.chromium.sdk.Breakpoint;
import org.chromium.sdk.Breakpoint.Target;
import org.chromium.sdk.JavascriptVm.BreakpointCallback;
import org.chromium.sdk.RelayOk;
import org.chromium.sdk.SyncCallback;
import org.chromium.sdk.TextStreamPosition;
import org.chromium.sdk.internal.wip.protocol.input.debugger.BreakpointResolvedEventData;
import org.chromium.sdk.util.RelaySyncCallback;

/**
 * A manager that works as factory for breakpoints.
 */
public class WipBreakpointManager {
  private final WipTabImpl tabImpl;
  private final AtomicInteger breakpointUniqueId = new AtomicInteger(0);
  private final Db db = new Db();

  WipBreakpointManager(WipTabImpl tabImpl) {
    this.tabImpl = tabImpl;
  }

  RelayOk setBreakpoint(Breakpoint.Target target, final int line, final int column,
      final boolean enabled, String condition,
      final BreakpointCallback callback, SyncCallback syncCallback) {

    int sdkId = breakpointUniqueId.getAndAdd(1);

    final WipBreakpointImpl breakpointImpl = new WipBreakpointImpl(this, sdkId,
        target, line, column, condition, enabled);

    db.addBreakpoint(breakpointImpl);

    if (enabled) {
      if (condition == null) {
        condition = "";
      }

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

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

      return WipBreakpointImpl.sendSetBreakpointRequest(target, line, column, condition,
          wrappedCallback, syncCallback, tabImpl.getCommandProcessor());
    } else {
      callback.success(breakpointImpl);
      return RelaySyncCallback.finish(syncCallback);
    }
  }

  Db getDb() {
    return db;
  }

  void clearNonProvisionalBreakpoints() {
    db.visitAllBreakpoints(new Db.Visitor<Void>() {
      @Override
      public Void visitAllBreakpoints(Set<WipBreakpointImpl> breakpoints) {
        List<WipBreakpointImpl> deleteList = new ArrayList<WipBreakpointImpl>();
        for (WipBreakpointImpl breakpoint : breakpoints) {
          if (breakpoint.getTarget().accept(IS_SCRIPT_ID_VISITOR)) {
            deleteList.add(breakpoint);
          } else {
            breakpoint.clearActualLocations();
          }
        }
        for (WipBreakpointImpl breakpoint : deleteList) {
          breakpoint.deleteSelfFromDb();
        }
        return null;
      }
    });
  }

  private static final Breakpoint.Target.Visitor<Boolean> IS_SCRIPT_ID_VISITOR =
      new Breakpoint.Target.Visitor<Boolean>() {
    @Override
    public Boolean visitScriptName(String scriptName) {
      return Boolean.FALSE;
    }

    @Override
    public Boolean visitScriptId(Object scriptId) {
      return Boolean.TRUE;
    }

    @Override
    public Boolean visitUnknown(Target target) {
      return Boolean.FALSE;
    }
  };

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

  Collection<WipBreakpointImpl> getAllBreakpoints() {
    return db.visitAllBreakpoints(new Db.Visitor<Collection<WipBreakpointImpl>>() {
      @Override
      public Collection<WipBreakpointImpl> visitAllBreakpoints(
          Set<WipBreakpointImpl> breakpoints) {
        return new ArrayList<WipBreakpointImpl>(breakpoints);
      }
    });
  }

  void breakpointReportedResolved(BreakpointResolvedEventData eventData) {
    String breakpointId = eventData.breakpointId();
    WipBreakpointImpl breakpoint = db.getBreakpoint(breakpointId);
    if (breakpoint == null) {
      throw new RuntimeException("Failed to find breakpoint by id: " + breakpointId);
    }
    breakpoint.addResolvedLocation(eventData.location());
  }

  // Accessed from Dispatch thread.
  public Collection<? extends Breakpoint> findRelatedBreakpoints(
      WipContextBuilder.WipDebugContextImpl.CallFrameImpl topFrame) {
    TextStreamPosition position = topFrame.getStatementStartPosition();
    int line = position.getLine();
    int column = position.getColumn();

    String scriptId = topFrame.getSourceId();
    final WipBreakpointImpl.ActualLocation location =
        new WipBreakpointImpl.ActualLocation(scriptId, line, Long.valueOf(column));

    return db.visitAllBreakpoints(new Db.Visitor<List<Breakpoint>>() {
      @Override
      public List<Breakpoint> visitAllBreakpoints(Set<WipBreakpointImpl> breakpoints) {
        List<Breakpoint> result = new ArrayList<Breakpoint>(1);
        for (WipBreakpointImpl breakpoint : breakpoints) {
          if (breakpoint.getActualLocations().contains(location)) {
            result.add(breakpoint);
          }
        }
        return result;
      }
    });
  }

  /**
   * Breakpoint data-base. Keeps track of all instances and their protocol-id -> instance mapping.
   * The name implies that it doesn't manage anything, only stores data.
   */
  static class Db {
    // Accessed from any thread.
    private final Set<WipBreakpointImpl> breakpoints = new HashSet<WipBreakpointImpl>();

    // Access from Dispatch thread only.
    private final Map<String, WipBreakpointImpl> idToBreakpoint =
        new HashMap<String, WipBreakpointImpl>();

    void addBreakpoint(WipBreakpointImpl breakpoint) {
      synchronized (breakpoints) {
        breakpoints.add(breakpoint);
      }
    }

    void removeBreakpoint(WipBreakpointImpl breakpoint) {
      synchronized (breakpoints) {
        breakpoints.remove(breakpoint);
      }
    }

    void setIdMapping(WipBreakpointImpl breakpoint,
        String protocolId) {
      if (protocolId == null) {
        idToBreakpoint.remove(protocolId);
      } else {
        idToBreakpoint.put(protocolId, breakpoint);
      }
    }

    WipBreakpointImpl getBreakpoint(String breakpointId) {
      return idToBreakpoint.get(breakpointId);
    }

    /**
     * Gives a synchronized access to all breakpoints.
     */
    <R> R visitAllBreakpoints(Visitor<R> visitor) {
      synchronized (breakpoints) {
        return visitor.visitAllBreakpoints(breakpoints);
      }
    }

    interface Visitor<R> {
      R visitAllBreakpoints(Set<WipBreakpointImpl> breakpoints);
    }
  }
}
