// 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 'dart:convert';
import 'dart:io';
import 'dart:math' as math;

import 'package:image/image.dart';
import 'package:path/path.dart';

String authorizationToken;

class UploadError extends Error {
  UploadError(this.message);
  final String message;
  @override
  String toString() => 'UploadError($message)';
}

void logMessage(String s) { print(s); }

class Upload {
  Upload(this.fromPath, this.largeName, this.smallName);

  static math.Random random;
  static const String uriAuthority = 'www.googleapis.com';
  static const String uriPath = 'upload/storage/v1/b/flutter-catalog/o';

  final String fromPath;
  final String largeName;
  final String smallName;

  List<int> largeImage;
  List<int> smallImage;
  bool largeImageSaved = false;
  int retryCount = 0;
  bool isComplete = false;

  // Exponential backoff per https://cloud.google.com/storage/docs/exponential-backoff
  Duration get timeLimit {
    if (retryCount == 0)
      return const Duration(milliseconds: 1000);
    random ??= math.Random();
    return Duration(milliseconds: random.nextInt(1000) + math.pow(2, retryCount) * 1000);
  }

  Future<bool> save(HttpClient client, String name, List<int> content) async {
    try {
      final Uri uri = Uri.https(uriAuthority, uriPath, <String, String>{
        'uploadType': 'media',
        'name': name,
      });
      final HttpClientRequest request = await client.postUrl(uri);
      request
        ..headers.contentType = ContentType('image', 'png')
        ..headers.add('Authorization', 'Bearer $authorizationToken')
        ..add(content);

      final HttpClientResponse response = await request.close().timeout(timeLimit);
      if (response.statusCode == HttpStatus.ok) {
        logMessage('Saved $name');
        await response.drain<void>();
      } else {
        // TODO(hansmuller): only retry on 5xx and 429 responses
        logMessage('Request to save "$name" (length ${content.length}) failed with status ${response.statusCode}, will retry');
        logMessage(await response.transform<String>(utf8.decoder).join());
      }
      return response.statusCode == HttpStatus.ok;
    } on TimeoutException catch (_) {
      logMessage('Request to save "$name" (length ${content.length}) timed out, will retry');
      return false;
    }
  }

  Future<bool> run(HttpClient client) async {
    assert(!isComplete);
    if (retryCount > 2)
      throw UploadError('upload of "$fromPath" to "$largeName" and "$smallName" failed after 2 retries');

    largeImage ??= await File(fromPath).readAsBytes();
    smallImage ??= encodePng(copyResize(decodePng(largeImage), 300));

    if (!largeImageSaved)
      largeImageSaved = await save(client, largeName, largeImage);
    isComplete = largeImageSaved && await save(client, smallName, smallImage);

    retryCount += 1;
    return isComplete;
  }

  static bool isNotComplete(Upload upload) => !upload.isComplete;
}

Future<void> saveScreenshots(List<String> fromPaths, List<String> largeNames, List<String> smallNames) async {
  assert(fromPaths.length == largeNames.length);
  assert(fromPaths.length == smallNames.length);

  List<Upload> uploads = List<Upload>(fromPaths.length);
  for (int index = 0; index < uploads.length; index += 1)
    uploads[index] = Upload(fromPaths[index], largeNames[index], smallNames[index]);

  while (uploads.any(Upload.isNotComplete)) {
    final HttpClient client = HttpClient();
    uploads = uploads.where(Upload.isNotComplete).toList();
    await Future.wait<bool>(uploads.map<Future<bool>>((Upload upload) => upload.run(client)));
    client.close(force: true);
  }
}


// If path is lib/foo.png then screenshotName is foo.
String screenshotName(String path) => basenameWithoutExtension(path);

Future<void> saveCatalogScreenshots({
    Directory directory, // Where the *.png screenshots are.
    String commit, // The commit hash to be used as a cloud storage "directory".
    String token, // Cloud storage authorization token.
    String prefix, // Prefix for all file names.
  }) async {
  final List<String> screenshots = <String>[];
  for (FileSystemEntity entity in directory.listSync()) {
    if (entity is File && entity.path.endsWith('.png')) {
      final File file = entity;
      screenshots.add(file.path);
    }
  }

  final List<String> largeNames = <String>[]; // Cloud storage names for the full res screenshots.
  final List<String> smallNames = <String>[]; // Likewise for the scaled down screenshots.
  for (String path in screenshots) {
    final String name = screenshotName(path);
    largeNames.add('$commit/$prefix$name.png');
    smallNames.add('$commit/$prefix${name}_small.png');
  }

  authorizationToken = token;
  await saveScreenshots(screenshots, largeNames, smallNames);
}