// Copyright 2016 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:convert';
import 'dart:isolate';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

typedef OnProgressListener = void Function(double completed, double total);
typedef OnResultListener = void Function(String result);

// An encapsulation of a large amount of synchronous processing.
//
// The choice of JSON parsing here is meant as an example that might surface
// in real-world applications.
class Calculator {
  Calculator({ @required this.onProgressListener, @required this.onResultListener, String data })
    : assert(onProgressListener != null),
      assert(onResultListener != null),
      // In order to keep the example files smaller, we "cheat" a little and
      // replicate our small json string into a 10,000-element array.
      _data = _replicateJson(data, 10000);

  final OnProgressListener onProgressListener;
  final OnResultListener onResultListener;
  final String _data;
  // This example assumes that the number of objects to parse is known in
  // advance. In a real-world situation, this might not be true; in that case,
  // the app might choose to display an indeterminate progress indicator.
  static const int _NUM_ITEMS = 110000;
  static const int _NOTIFY_INTERVAL = 1000;

  // Run the computation associated with this Calculator.
  void run() {
    int i = 0;
    final JsonDecoder decoder = JsonDecoder(
      (dynamic key, dynamic value) {
        if (key is int && i++ % _NOTIFY_INTERVAL == 0)
          onProgressListener(i.toDouble(), _NUM_ITEMS.toDouble());
        return value;
      }
    );
    try {
      final List<dynamic> result = decoder.convert(_data);
      final int n = result.length;
      onResultListener('Decoded $n results');
    } catch (e, stack) {
      print('Invalid JSON file: $e');
      print(stack);
    }
  }

  static String _replicateJson(String data, int count) {
    final StringBuffer buffer = StringBuffer()..write('[');
    for (int i = 0; i < count; i++) {
      buffer.write(data);
      if (i < count - 1)
        buffer.write(',');
    }
    buffer.write(']');
    return buffer.toString();
  }
}

// The current state of the calculation.
enum CalculationState {
  idle,
  loading,
  calculating
}

// Structured message to initialize the spawned isolate.
class CalculationMessage {
  CalculationMessage(this.data, this.sendPort);
  String data;
  SendPort sendPort;
}

// A manager for the connection to a spawned isolate.
//
// Isolates communicate with each other via ReceivePorts and SendPorts.
// This class manages these ports and maintains state related to the
// progress of the background computation.
class CalculationManager {
  CalculationManager({ @required this.onProgressListener, @required this.onResultListener })
    : assert(onProgressListener != null),
      assert(onResultListener != null),
      _receivePort = ReceivePort() {
    _receivePort.listen(_handleMessage);
  }

  CalculationState _state = CalculationState.idle;
  CalculationState get state => _state;
  bool get isRunning => _state != CalculationState.idle;

  double _completed = 0.0;
  double _total = 1.0;

  final OnProgressListener onProgressListener;
  final OnResultListener onResultListener;

  // Start the background computation.
  //
  // Does nothing if the computation is already running.
  void start() {
    if (!isRunning) {
      _state = CalculationState.loading;
      _runCalculation();
    }
  }

  // Stop the background computation.
  //
  // Kills the isolate immediately, if spawned. Does nothing if the
  // computation is not running.
  void stop() {
    if (isRunning) {
      _state = CalculationState.idle;
      if (_isolate != null) {
        _isolate.kill(priority: Isolate.immediate);
        _isolate = null;
        _completed = 0.0;
        _total = 1.0;
      }
    }
  }

  final ReceivePort _receivePort;
  Isolate _isolate;

  void _runCalculation() {
    // Load the JSON string. This is done in the main isolate because spawned
    // isolates do not have access to the root bundle. However, the loading
    // process is asynchronous, so the UI will not block while the file is
    // loaded.
    rootBundle.loadString('services/data.json').then<void>((String data) {
      if (isRunning) {
        final CalculationMessage message = CalculationMessage(data, _receivePort.sendPort);
        // Spawn an isolate to JSON-parse the file contents. The JSON parsing
        // is synchronous, so if done in the main isolate, the UI would block.
        Isolate.spawn<CalculationMessage>(_calculate, message).then<void>((Isolate isolate) {
          if (!isRunning) {
            isolate.kill(priority: Isolate.immediate);
          } else {
            _state = CalculationState.calculating;
            _isolate = isolate;
          }
        });
      }
    });
  }

  void _handleMessage(dynamic message) {
    if (message is List<double>) {
      _completed = message[0];
      _total = message[1];
      onProgressListener(_completed, _total);
    } else if (message is String) {
      _completed = 0.0;
      _total = 1.0;
      _isolate = null;
      _state = CalculationState.idle;
      onResultListener(message);
    }
  }

  // Main entry point for the spawned isolate.
  //
  // This entry point must be static, and its (single) argument must match
  // the message passed in Isolate.spawn above. Typically, some part of the
  // message will contain a SendPort so that the spawned isolate can
  // communicate back to the main isolate.
  //
  // Static and global variables are initialized anew in the spawned isolate,
  // in a separate memory space.
  static void _calculate(CalculationMessage message) {
    final SendPort sender = message.sendPort;
    final Calculator calculator = Calculator(
      onProgressListener: (double completed, double total) {
        sender.send(<double>[ completed, total ]);
      },
      onResultListener: sender.send,
      data: message.data
    );
    calculator.run();
  }
}

// Main app widget.
//
// The app shows a simple UI that allows control of the background computation,
// as well as an animation to illustrate that the UI does not block while this
// computation is performed.
//
// This is a StatefulWidget in order to hold the CalculationManager and
// the AnimationController for the running animation.
class IsolateExampleWidget extends StatefulWidget {
  @override
  IsolateExampleState createState() => IsolateExampleState();
}

// Main application state.
class IsolateExampleState extends State<StatefulWidget> with SingleTickerProviderStateMixin {

  String _status = 'Idle';
  String _label = 'Start';
  String _result = ' ';
  double _progress = 0.0;
  AnimationController _animation;
  CalculationManager _calculationManager;

  @override
  void initState() {
    super.initState();
    _animation = AnimationController(
      duration: const Duration(milliseconds: 3600),
      vsync: this,
    )..repeat();
    _calculationManager = CalculationManager(
      onProgressListener: _handleProgressUpdate,
      onResultListener: _handleResult
    );
  }

  @override
  void dispose() {
    _animation.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Material(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceAround,
        children: <Widget>[
          RotationTransition(
            turns: _animation,
            child: Container(
              width: 120.0,
              height: 120.0,
              color: const Color(0xFF882222),
            )
          ),
          Opacity(
            opacity: _calculationManager.isRunning ? 1.0 : 0.0,
            child: CircularProgressIndicator(
              value: _progress
            )
          ),
          Text(_status),
          Center(
            child: RaisedButton(
              child: Text(_label),
              onPressed: _handleButtonPressed
            )
          ),
          Text(_result)
        ]
      )
    );
  }

  void _handleProgressUpdate(double completed, double total) {
    _updateState(' ', completed / total);
  }

  void _handleResult(String result) {
    _updateState(result, 0.0);
  }

  void _handleButtonPressed() {
    if (_calculationManager.isRunning)
      _calculationManager.stop();
    else
      _calculationManager.start();
    _updateState(' ', 0.0);
  }

  String _getStatus(CalculationState state) {
      switch (state) {
        case CalculationState.loading:
          return 'Loading...';
        case CalculationState.calculating:
          return 'In Progress';
        case CalculationState.idle:
        default:
          return 'Idle';
      }
  }

  void _updateState(String result, double progress) {
    setState(() {
      _result = result;
      _progress = progress;
      _label = _calculationManager.isRunning ? 'Stop' : 'Start';
      _status = _getStatus(_calculationManager.state);
    });
  }
}

void main() {
  runApp(MaterialApp(home: IsolateExampleWidget()));
}
