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

import 'dart:async';

import 'context.dart';
import 'io.dart';

const PortScanner _kLocalPortScanner = const HostPortScanner();
const int _kMaxSearchIterations = 20;

PortScanner get portScanner {
  return context == null
      ? _kLocalPortScanner
      : context.putIfAbsent(PortScanner, () => _kLocalPortScanner);
}

abstract class PortScanner {
  const PortScanner();

  /// Returns true if the specified [port] is available to bind to.
  Future<bool> isPortAvailable(int port);

  /// Returns an available ephemeral port.
  Future<int> findAvailablePort();

  /// Returns an available port as close to [defaultPort] as possible.
  ///
  /// If [defaultPort] is available, this will return it. Otherwise, it will
  /// search for an avaiable port close to [defaultPort]. If it cannot find one,
  /// it will return any available port.
  Future<int> findPreferredPort(int defaultPort, { int searchStep: 2 }) async {
    int iterationCount = 0;

    while (iterationCount < _kMaxSearchIterations) {
      final int port = defaultPort + iterationCount * searchStep;
      if (await isPortAvailable(port))
        return port;
      iterationCount++;
    }

    return findAvailablePort();
  }
}

class HostPortScanner extends PortScanner {
  const HostPortScanner();

  @override
  Future<bool> isPortAvailable(int port) async {
    try {
      // TODO(ianh): This is super racy.
      final ServerSocket socket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, port);
      await socket.close();
      return true;
    } catch (error) {
      return false;
    }
  }

  @override
  Future<int> findAvailablePort() async {
    final ServerSocket socket = await ServerSocket.bind(InternetAddress.LOOPBACK_IP_V4, 0);
    final int port = socket.port;
    await socket.close();
    return port;
  }
}
