import 'dart:html' as html;
import 'dart:math';

import 'package:rules_of_living/src/Simulation.dart';

class Engine {
  // Elapsed Time Counter - useful for Safety Timeout
  Stopwatch _elapsed = new Stopwatch();

  /// Game Tick Rate
  ///
  /// *does* impact game speed; dictates how long each logic step takes. Only
  /// interesting for engine internal calculations, to set simulation speed
  /// from the outside use stepsPerSecond instead.
  int _MS_PER_STEP = 1000 ~/ 3;

  /// Set Logic Updates per Second
  ///
  /// Dictates simulation speed. Sets the amount of (logic) updates per second.
  /// Translations between number of updates and their timings is not exact so
  /// comparing this variable to fixed ints might not yield the expected results.
  /// Does not affect render frequency, which is handled by framesPerSecond.
  int get stepsPerSecond => 1000 ~/ _MS_PER_STEP;
  set stepsPerSecond(int val) => _MS_PER_STEP = 1000 ~/ val;

  // Max Frame (i.e. Rendering) rate - does *not* impact game speed
  final int _MS_PER_FRAME = 1000 ~/ 30;

  // ms stuck in updateloop after which game will declare itself unresponsive
  final int SAFETY_TIMEOUT = 2000;

  /// Grid Size
  ///
  /// Number of cells on x coordinate and y coordinate. Can be set individually.
  Point get gridSize => Point<int>(_simulation.w, _simulation.h);
  void set gridSize(Point<int> value) {
    if (value.x <= 0 || value.y <= 0)
      throw ArgumentError("grid size must not be smaller than 1");
    _simulation = Simulation(value.x, value.y);
  }

  num _updateLag = 0.0;
  num _drawLag = 0.0;

  /// The Canvas to Paint on
  ///
  /// Manually define or change the canvas the engine should render to. Should
  /// be used if no canvas was defined at engine creation and it should be
  /// rendered later.
  html.CanvasElement canvas;
  Simulation _simulation;
  bool running = false;

  Engine([x = 100, y = 100, this.canvas]) {
    _simulation = Simulation(x, y);

    _elapsed.start();
    _simulation.addPattern(amount: 15, dispersal: 5);
    html.window.animationFrame.then(animFrame);
  }

  void animFrame(num now) {
    process(now);
    html.window.animationFrame.then(animFrame);
  }

  void reset() {
    _simulation.reset();
    running = false;
  }

  void clear() {
    _simulation = new Simulation(gridSize.x, gridSize.y);
    running = false;
  }

  void process(num now) {
    _drawLag += _elapsed.elapsedMilliseconds;
    _updateLag += _elapsed.elapsedMilliseconds;
    _elapsed.reset();

    while (_updateLag >= _MS_PER_STEP) {
      if (_elapsed.elapsedMilliseconds > SAFETY_TIMEOUT) {
        // TODO stub - give warning etc when this occurs
        print("ERROR STUCK IN UPDATE LOOP");
        break;
      }
      if (running == true) update();
      _updateLag -= _MS_PER_STEP;
    }

    if (_drawLag >= _MS_PER_FRAME) {
      render(_updateLag / _MS_PER_STEP);
      _drawLag = 0;
    }
  }

  /// Update Engine Logic
  ///
  /// Updates the logic of the engine by one tick. Should usually not be called
  /// directly, since it is automatically taken care of by the processing function.
  /// If simulation should be advanced manually one time, prefer using step().
  void update() {
    Map<int, bool> simulationUpdate = _simulation.update();
    _simulation.mergeStateChanges(simulationUpdate);

    if (simulationUpdate.length == 0) running = false;
  }

  /// Advances Logic One Update
  ///
  /// Moves logic of the engine one step forward and pauses the
  /// simulation. Does not automatically re-render the new state
  /// (though this should usually not pose a problem).
  void step() {
    update();
    running = false;
  }

  /// Renders the Current Simulation State
  ///
  /// Renders the simulation once. Will usually automatically be called by
  /// the internal engine processing. Does not do anything if no canvas is
  /// defined.
  void render([num interp]) {
    if (canvas == null) return;

    _simulation.render(canvas, interp);
  }

  void addPattern({int amount, int dispersal}) {
    _simulation.addPattern(amount: amount, dispersal: dispersal);
  }

  void toggleEdgeRendering() {
    _simulation.renderEdges = !_simulation.renderEdges;
  }
}