/*
 * Copyright 2017 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 io.flutter.run;

import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.PathUtil;
import com.intellij.xdebugger.XDebuggerUtil;
import com.intellij.xdebugger.XSourcePosition;
import com.jetbrains.lang.dart.DartFileType;
import gnu.trove.THashMap;
import gnu.trove.TIntObjectHashMap;
import io.flutter.vmService.DartVmServiceDebugProcess;
import org.dartlang.vm.service.element.Script;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * A specific version of a Dart file, as downloaded from Observatory.
 * <p>
 * Corresponds to a
 * <a href="https://github.com/dart-lang/sdk/blob/master/runtime/vm/service/service.md#script">Script</a>
 * in the Observatory API. A new version will be generated after a hot reload (with a different script id).
 * <p>
 * See
 */
class ObservatoryFile {
  /**
   * Maps an observatory token id to its line and column.
   */
  @Nullable
  private final TIntObjectHashMap<Position> positionMap;

  /**
   * User-visible source code downloaded from Observatory.
   * <p>
   * The LightVirtualFile has no parent directory so its name will be something like /foo.dart.
   * Since it has no location, breakpoints can't be set in this file.
   * <p>
   * This will be null if not requested when the ObservatoryFile was constructed.
   */
  @Nullable
  private final LightVirtualFile snapshot;

  ObservatoryFile(@NotNull Script script, boolean wantSnapshot) {
    final @Nullable List<List<Integer>> tokenPosTable = script.getTokenPosTable();
    if (tokenPosTable != null) {
      positionMap = createPositionMap(tokenPosTable);
    }
    else {
      positionMap = null;
    }
    snapshot = wantSnapshot ? createSnapshot(script) : null;
  }

  boolean hasSnapshot() {
    return snapshot != null;
  }

  /**
   * Given a token id, returns the source position to display to the user.
   * <p>
   * If no local file was provided, uses the snapshot if available. (However, in that
   * case, breakpoints won't work.)
   */
  @Nullable
  XSourcePosition createPosition(@Nullable VirtualFile local, int tokenPos) {
    final VirtualFile fileToUse = local == null ? snapshot : local;
    if (fileToUse == null) return null;

    if (positionMap == null) {
      return null;
    }

    final Position pos = positionMap.get(tokenPos);
    if (pos == null) {
      return XDebuggerUtil.getInstance().createPositionByOffset(fileToUse, 0);
    }
    return XDebuggerUtil.getInstance().createPosition(fileToUse, pos.line, pos.column);
  }

  /**
   * Unpacks a position token table into a map from position id to Position.
   * <p>
   * <p>See <a href="https://github.com/dart-lang/vm_service_drivers/blob/master/dart/tool/service.md#scrip">docs</a>.
   */
  @NotNull
  private static TIntObjectHashMap<Position> createPositionMap(@NotNull final List<List<Integer>> table) {
    final TIntObjectHashMap<Position> result = new TIntObjectHashMap<>();

    for (List<Integer> line : table) {
      // Each line consists of a line number followed by (tokenId, columnNumber) pairs.
      // Both lines and columns are one-based.
      final Iterator<Integer> items = line.iterator();

      // Convert line number from one-based to zero-based.
      final int lineNumber = Math.max(0, items.next() - 1);
      while (items.hasNext()) {
        final int tokenId = items.next();
        // Convert column from one-based to zero-based.
        final int column = Math.max(0, items.next() - 1);
        result.put(tokenId, new Position(lineNumber, column));
      }
    }
    return result;
  }

  @Nullable
  private static LightVirtualFile createSnapshot(@NotNull Script script) {
    // LightVirtualFiles have no parent directory, so just use the filename.
    final String filename = PathUtil.getFileName(script.getUri());
    final String scriptSource = script.getSource();
    if (scriptSource == null) {
      return null;
    }

    final LightVirtualFile snapshot = new LightVirtualFile(filename, DartFileType.INSTANCE, scriptSource);
    snapshot.setWritable(false);
    return snapshot;
  }

  /**
   * A per-isolate cache of Observatory files.
   */
  static class Cache {
    @NotNull
    private final String isolateId;

    @NotNull
    private final DartVmServiceDebugProcess.ScriptProvider provider;

    /**
     * A cache containing each file downloaded from Observatory. The key is a script id.
     * Each version of a file is stored as a separate entry.
     */
    private final Map<String, ObservatoryFile> versions = new THashMap<>();

    Cache(@NotNull String isolateId, @NotNull DartVmServiceDebugProcess.ScriptProvider provider) {
      this.isolateId = isolateId;
      this.provider = provider;
    }

    /**
     * Returns an observatory file, optionally containing a snapshot.
     * <p>
     * Downloads it if not in the cache.
     * <p>
     * Returns null if not available.
     */
    @Nullable
    ObservatoryFile downloadOrGet(@NotNull String scriptId, boolean wantSnapshot) {
      final ObservatoryFile cached = this.versions.get(scriptId);
      if (cached != null && (cached.hasSnapshot() || !wantSnapshot)) {
        return cached;
      }

      final Script script = provider.downloadScript(isolateId, scriptId);
      if (script == null) return null;

      final ObservatoryFile downloaded = new ObservatoryFile(script, wantSnapshot);
      this.versions.put(scriptId, downloaded);

      if (wantSnapshot && !downloaded.hasSnapshot()) {
        return null;
      }
      return downloaded;
    }
  }

  private static class Position {
    final int line; // zero-based
    final int column; // zero-based

    Position(int line, int column) {
      this.line = line;
      this.column = column;
    }
  }
}