part of game;

final double _gameSizeWidth = 320.0;
double _gameSizeHeight = 320.0;

final bool _drawDebug = false;

class GameDemoNode extends NodeWithSize {

  GameDemoNode(
    this._images,
    this._spritesGame,
    this._spritesUI,
    this._sounds,
    this._gameOverCallback
  ): super(new Size(320.0, 320.0)) {
    // Add background
    _background = new RepeatedImage(_images["assets/starfield.png"]);
    addChild(_background);

    // Create starfield
    _starField = new StarField(_spritesGame, 200);
    addChild(_starField);

    // Add nebula
    _nebula = new RepeatedImage(_images["assets/nebula.png"], sky.TransferMode.plus);
    addChild(_nebula);

    // Setup game screen, it will always be anchored to the bottom of the screen
    _gameScreen = new Node();
    addChild(_gameScreen);

    // Setup the level and add it to the screen, the level is the node where
    // all our game objects live. It is moved to scroll the game
    _level = new Level();
    _gameScreen.addChild(_level);

    _objectFactory = new GameObjectFactory(_spritesGame, _sounds, _level);

    _level.ship = new Ship(_objectFactory);
    _level.addChild(_level.ship);

    // Add the joystick
    _joystick = new VirtualJoystick();
    _gameScreen.addChild(_joystick);

    // Add HUD
    _hud = new Hud(_spritesUI);
    addChild(_hud);

    // Add initial game objects
    addObjects();
  }

  // Resources
  ImageMap _images;
  Map<String, SoundEffect> _sounds;
  SpriteSheet _spritesGame;
  SpriteSheet _spritesUI;

  // Sounds
  SoundEffectPlayer _effectPlayer = SoundEffectPlayer.sharedInstance();

  // Callback
  Function _gameOverCallback;

  // Game screen nodes
  Node _gameScreen;
  VirtualJoystick _joystick;

  GameObjectFactory _objectFactory;
  Level _level;
  StarField _starField;
  RepeatedImage _background;
  RepeatedImage _nebula;
  Hud _hud;

  // Game properties
  double _scrollSpeed = 2.0;
  double _scroll = 0.0;

  int _framesToFire = 0;
  int _framesBetweenShots = 20;

  bool _gameOver = false;

  void spriteBoxPerformedLayout() {
    _gameSizeHeight = spriteBox.visibleArea.height;
    _gameScreen.position = new Point(0.0, _gameSizeHeight);
  }

  void update(double dt) {
    // Scroll the level
    _scroll = _level.scroll(_scrollSpeed);
    _starField.move(0.0, _scrollSpeed);

    _background.move(_scrollSpeed * 0.1);
    _nebula.move(_scrollSpeed);

    // Add objects
    addObjects();

    // Move the ship
    if (!_gameOver) {
      _level.ship.applyThrust(_joystick.value, _scroll);
    }

    // Add shots
    if (_framesToFire == 0 && _joystick.isDown && !_gameOver) {
      fire();
      _framesToFire = _framesBetweenShots;
    }
    if (_framesToFire > 0) _framesToFire--;

    // Move game objects
    for (Node node in _level.children) {
      if (node is GameObject) {
        node.move();
      }
    }

    // Remove offscreen game objects
    for (int i = _level.children.length - 1; i >= 0; i--) {
      Node node = _level.children[i];
      if (node is GameObject) {
        node.removeIfOffscreen(_scroll);
      }
    }

    if (_gameOver) return;

    // Check for collisions between lasers and objects that can take damage
    List<Laser> lasers = [];
    for (Node node in _level.children) {
      if (node is Laser) lasers.add(node);
    }

    List<GameObject> damageables = [];
    for (Node node in _level.children) {
      if (node is GameObject && node.canBeDamaged) damageables.add(node);
    }

    for (Laser laser in lasers) {
      for (GameObject damageable in damageables) {
        if (laser.collidingWith(damageable)) {
          // Hit something that can take damage
          _hud.score += damageable.addDamage(laser.impact);
          laser.destroy();
        }
      }
    }

    // Check for collsions between ship and objects that can damage the ship
    List<Node> nodes = new List.from(_level.children);
    for (Node node in nodes) {
      if (node is GameObject && node.canDamageShip) {
        if (node.collidingWith(_level.ship)) {
          // The ship was hit :(
          killShip();
          _level.ship.visible = false;
        }
      }
    }
  }

  int _chunk = 0;
  double _chunkSpacing = 640.0;

  void addObjects() {

    while (_scroll + _chunkSpacing >= _chunk * _chunkSpacing) {
      addLevelChunk(
        _chunk,
        -_chunk * _chunkSpacing - _chunkSpacing);

      _chunk += 1;
    }
  }

  void addLevelChunk(int chunk, double yPos) {
    if (chunk == 0) {
      // Leave the first chunk empty
      return;
    } else if (chunk == 1) {
      addLevelAsteroids(10, yPos, 0.0);
    } else {
      addLevelAsteroids(9 + chunk, yPos, 0.5);
    }
  }

  void addLevelAsteroids(int numAsteroids, double yPos, double distribution) {
    for (int i = 0; i < numAsteroids; i++) {
      GameObjectType type = (randomDouble() < distribution) ? GameObjectType.asteroidBig : GameObjectType.asteroidSmall;
      Point pos = new Point(randomSignedDouble() * 160.0,
                            yPos + _chunkSpacing * randomDouble());
      _objectFactory.addGameObject(type, pos);
    }
    _objectFactory.addGameObject(GameObjectType.movingEnemy, new Point(0.0, yPos + 160.0));
  }

  void fire() {
    Laser shot0 = new Laser(_objectFactory);
    shot0.position = _level.ship.position + new Offset(17.0, -10.0);
    _level.addChild(shot0);

    Laser shot1 = new Laser(_objectFactory);
    shot1.position = _level.ship.position + new Offset(-17.0, -10.0);
    _level.addChild(shot1);

    _effectPlayer.play(_sounds["laser"]);
  }

  void killShip() {
    // Hide ship
    _level.ship.visible = false;

    _effectPlayer.play(_sounds["explosion"]);

    // Add explosion
    Explosion explo = new Explosion(_spritesGame);
    explo.scale = 1.5;
    explo.position = _level.ship.position;
    _level.addChild(explo);

    // Add flash
    Flash flash = new Flash(size, 1.0);
    addChild(flash);

    // Set the state to game over
    _gameOver = true;

    // Return to main scene and report the score back in 2 seconds
    new Timer(new Duration(seconds: 2), () { _gameOverCallback(_hud.score); });
  }
}

class Level extends Node {
  Level() {
    position = new Point(160.0, 0.0);
  }

  Ship ship;

  double scroll(double scrollSpeed) {
    position += new Offset(0.0, scrollSpeed);
    return position.y;
  }
}

abstract class GameObject extends Node {
  double radius = 0.0;
  double removeLimit = 1280.0;
  bool canDamageShip = true;
  bool canBeDamaged = true;
  double maxDamage = 3.0;
  double damage = 0.0;

  Paint _paintDebug = new Paint()
    ..color=new Color(0xffff0000)
    ..strokeWidth = 1.0
    ..setStyle(sky.PaintingStyle.stroke);

  bool collidingWith(GameObject obj) {
    return (GameMath.pointQuickDist(position, obj.position)
      < radius + obj.radius);
  }

  void move() {
  }

  void removeIfOffscreen(double scroll) {
    ;
    if (-position.y > scroll + removeLimit ||
        -position.y < scroll - 50.0) {
      removeFromParent();
    }
  }

  void destroy() {
    if (parent != null) {
      Explosion explo = createExplosion();
      if (explo != null) {
        explo.position = position;
        parent.addChild(explo);
      }

      removeFromParent();
    }
  }

  int addDamage(double d) {
    if (!canBeDamaged) return 0;

    damage += d;
    if (damage >= maxDamage) {
      destroy();
      return (maxDamage * 10).ceil();
    }
    return 10;
  }

  Explosion createExplosion() {
    return null;
  }

  void paint(PaintingCanvas canvas) {
    if (_drawDebug) {
      canvas.drawCircle(Point.origin, radius, _paintDebug);
    }
    super.paint(canvas);
  }

  void setupActions() {
  }
}

class Ship extends GameObject {
  Ship(GameObjectFactory f) {
    // Add main ship sprite
    _sprt = new Sprite(f.sheet["ship.png"]);
    _sprt.scale = 0.3;
    _sprt.rotation = -90.0;
    addChild(_sprt);
    radius = 20.0;

    canBeDamaged = false;
    canDamageShip = false;

    // Set start position
    position = new Point(0.0, 50.0);
  }

  Sprite _sprt;

  void applyThrust(Point joystickValue, double scroll) {
    Point oldPos = position;
    Point target = new Point(joystickValue.x * 160.0, joystickValue.y * 220.0 - 250.0 - scroll);
    double filterFactor = 0.2;

    position = new Point(
      GameMath.filter(oldPos.x, target.x, filterFactor),
      GameMath.filter(oldPos.y, target.y, filterFactor));
  }
}

class Laser extends GameObject {
  double impact = 1.0;

  Laser(GameObjectFactory f) {
    // Add sprite
    _sprt = new Sprite(f.sheet["laser.png"]);
    _sprt.scale = 0.3;
    _sprt.transferMode = sky.TransferMode.plus;
    addChild(_sprt);
    radius = 10.0;
    removeLimit = 640.0;

    canDamageShip = false;
    canBeDamaged = false;
  }

  Sprite _sprt;

  void move() {
    position += new Offset(0.0, -10.0);
  }
}

abstract class Obstacle extends GameObject {

  Obstacle(this._f);

  double explosionScale = 1.0;
  GameObjectFactory _f;

  Explosion createExplosion() {
    SoundEffectPlayer.sharedInstance().play(_f.sounds["explosion"]);
    Explosion explo = new Explosion(_f.sheet);
    explo.scale = explosionScale;
    return explo;
  }
}

abstract class Asteroid extends Obstacle {
  Asteroid(GameObjectFactory f) : super(f);

  Sprite _sprt;

  void setupActions() {
    // Rotate obstacle
    int direction = 1;
    if (randomDouble() < 0.5) direction = -1;
    ActionTween rotate = new ActionTween(
      (a) => _sprt.rotation = a,
      0.0, 360.0 * direction, 5.0 + 5.0 * randomDouble());
    _sprt.actions.run(new ActionRepeatForever(rotate));
  }

  set damage(double d) {
    super.damage = d;
    int alpha = ((200.0 * d) ~/ maxDamage).clamp(0, 200);
    _sprt.colorOverlay = new Color.fromARGB(alpha, 255, 3, 86);
  }
}

class AsteroidBig extends Asteroid {
  AsteroidBig(GameObjectFactory f) : super(f) {
    _sprt = new Sprite(f.sheet["asteroid_big_${randomInt(3)}.png"]);
    _sprt.scale = 0.3;
    radius = 25.0;
    maxDamage = 5.0;
    addChild(_sprt);
  }
}

class AsteroidSmall extends Asteroid {
  AsteroidSmall(GameObjectFactory f) : super(f) {
    _sprt = new Sprite(f.sheet["asteroid_small_${randomInt(3)}.png"]);
    _sprt.scale = 0.3;
    radius = 12.0;
    maxDamage = 3.0;
    addChild(_sprt);
  }
}

class MovingEnemy extends Obstacle {
  MovingEnemy(GameObjectFactory f) : super(f) {
    _sprt = new Sprite(f.sheet["ship.png"]);
    _sprt.scale = 0.2;
    radius = 12.0;
    maxDamage = 2.0;
    addChild(_sprt);

    constraints = [new ConstraintRotationToMovement(0.0, 0.5)];
  }

  void setupActions() {
    List<Offset> offsets = [
      new Offset(-160.0, 160.0),
      new Offset(-80.0, -160.0),
      new Offset(0.0, 160.0),
      new Offset(80.0, -160.0),
      new Offset(160.0, 160.0)];

    List<Point> points = [];
    for (Offset offset in offsets) {
      points.add(position + offset);
    }

    ActionSpline spline = new ActionSpline((a) => position = a, points, 4.0);
    actions.run(new ActionRepeatForever(spline));
  }

  Sprite _sprt;
}

enum GameObjectType {
  asteroidBig,
  asteroidSmall,
  movingEnemy,
}

class GameObjectFactory {
  GameObjectFactory(this.sheet, this.sounds, this.level);

  SpriteSheet sheet;
  Map<String,SoundEffect> sounds;
  Level level;

  void addGameObject(GameObjectType type, Point pos) {
    GameObject obj;
    if (type == GameObjectType.asteroidBig)
      obj = new AsteroidBig(this);
    else if (type == GameObjectType.asteroidSmall)
      obj = new AsteroidSmall(this);
    else if (type == GameObjectType.movingEnemy)
      obj = new MovingEnemy(this);

    obj.position = pos;
    obj.setupActions();

    level.addChild(obj);
  }
}

// class MovingObstacle extends Obstacle {
//   MovingObstacle(SpriteSheet sheet, Map<String,SoundEffect> effects, ObstacleType type) : super (sheet, effects, type);
//
//   void setupAction() {
//     actions.stopAll();
//
//     List<Offset> offsets = [
//       new Offset(-160.0, 160.0),
//       new Offset(-80.0, -160.0),
//       new Offset(0.0, 160.0),
//       new Offset(80.0, -160.0),
//       new Offset(160.0, 160.0)];
//
//     List<Point> points = [];
//     for (Offset offset in offsets) {
//       points.add(position + offset);
//     }
//
//     ActionSpline spline = new ActionSpline((a) => position = a, points, 4.0);
//     actions.run(new ActionRepeatForever(spline));
//   }
// }

class StarField extends NodeWithSize {
  sky.Image _image;
  SpriteSheet _spriteSheet;
  int _numStars;
  bool _autoScroll;

  List<Point> _starPositions;
  List<double> _starScales;
  List<Rect> _rects;
  List<Color> _colors;

  final double _padding = 50.0;
  Size _paddedSize = Size.zero;

  Paint _paint = new Paint()
    ..setFilterQuality(sky.FilterQuality.low)
    ..isAntiAlias = false
    ..setTransferMode(sky.TransferMode.plus);

  StarField(this._spriteSheet, this._numStars, [this._autoScroll = false]) : super(Size.zero) {
    _image = _spriteSheet.image;
  }

  void addStars() {
    _starPositions = [];
    _starScales = [];
    _colors = [];
    _rects = [];

    size = spriteBox.visibleArea.size;
    _paddedSize = new Size(size.width + _padding * 2.0,
                           size.height + _padding * 2.0);

    for (int i  = 0; i < _numStars; i++) {
      _starPositions.add(new Point(randomDouble() * _paddedSize.width,
                                   randomDouble() * _paddedSize.height));
      _starScales.add(randomDouble() * 0.4);
      _colors.add(new Color.fromARGB((255.0 * (randomDouble() * 0.5 + 0.5)).toInt(), 255, 255, 255));
      _rects.add(_spriteSheet["star_${randomInt(2)}.png"].frame);
    }
  }

  void spriteBoxPerformedLayout() {
    addStars();
  }

  void paint(PaintingCanvas canvas) {
    // Create a transform for each star
    List<sky.RSTransform> transforms = [];
    for (int i = 0; i < _numStars; i++) {
      sky.RSTransform transform = new sky.RSTransform(
        _starScales[i],
        0.0,
        _starPositions[i].x - _padding,
        _starPositions[i].y - _padding);

      transforms.add(transform);
    }

    // Draw the stars
    canvas.drawAtlas(_image, transforms, _rects, _colors, sky.TransferMode.modulate, null, _paint);
  }

  void move(double dx, double dy) {
    for (int i  = 0; i < _numStars; i++) {
      double xPos = _starPositions[i].x;
      double yPos = _starPositions[i].y;
      double scale = _starScales[i];

      xPos += dx * scale;
      yPos += dy * scale;

      if (xPos >= _paddedSize.width) xPos -= _paddedSize.width;
      if (xPos < 0) xPos += _paddedSize.width;
      if (yPos >= _paddedSize.height) yPos -= _paddedSize.height;
      if (yPos < 0) yPos += _paddedSize.height;

      _starPositions[i] = new Point(xPos, yPos);
    }
  }

  void update(double dt) {
    if (_autoScroll) {
      move(0.0, dt * 100.0);
    }
  }
}

class RepeatedImage extends Node {
  Sprite _sprt0;
  Sprite _sprt1;

  RepeatedImage(sky.Image image, [sky.TransferMode mode = null]) {
    _sprt0 = new Sprite.fromImage(image);
    _sprt0.size = new Size(1024.0, 1024.0);
    _sprt0.pivot = Point.origin;
    _sprt1 = new Sprite.fromImage(image);
    _sprt1.size = new Size(1024.0, 1024.0);
    _sprt1.pivot = Point.origin;
    _sprt1.position = new Point(0.0, -1024.0);

    if (mode != null) {
      _sprt0.transferMode = mode;
      _sprt1.transferMode = mode;
    }

    addChild(_sprt0);
    addChild(_sprt1);
  }

  void move(double dy) {
    double yPos = (position.y + dy) % 1024.0;
    position = new Point(0.0, yPos);
  }
}

class Explosion extends Node {
  Explosion(SpriteSheet sheet) {
    // Add particles
    ParticleSystem particlesDebris = new ParticleSystem(
      sheet["explosion_particle.png"],
      rotateToMovement: true,
      startRotation:90.0,
      startRotationVar: 0.0,
      endRotation: 90.0,
      startSize: 0.3,
      startSizeVar: 0.1,
      endSize: 0.3,
      endSizeVar: 0.1,
      numParticlesToEmit: 25,
      emissionRate:1000.0,
      greenVar: 127,
      redVar: 127
    );
    particlesDebris.zPosition = 1010.0;
    addChild(particlesDebris);

    ParticleSystem particlesFire = new ParticleSystem(
      sheet["fire_particle.png"],
      colorSequence: new ColorSequence([new Color(0xffffff33), new Color(0xffff3333), new Color(0x00ff3333)], [0.0, 0.5, 1.0]),
      numParticlesToEmit: 25,
      emissionRate: 1000.0,
      startSize: 0.5,
      startSizeVar: 0.1,
      endSize: 0.5,
      endSizeVar: 0.1,
      posVar: new Point(10.0, 10.0),
      speed: 10.0,
      speedVar: 5.0
    );
    particlesFire.zPosition = 1011.0;
    addChild(particlesFire);

    // Add ring
    Sprite sprtRing = new Sprite(sheet["explosion_ring.png"]);
    sprtRing.transferMode = sky.TransferMode.plus;
    addChild(sprtRing);

    Action scale = new ActionTween( (a) => sprtRing.scale = a, 0.2, 1.0, 1.5);
    Action scaleAndRemove = new ActionSequence([scale, new ActionRemoveNode(sprtRing)]);
    Action fade = new ActionTween( (a) => sprtRing.opacity = a, 1.0, 0.0, 1.5);
    actions.run(scaleAndRemove);
    actions.run(fade);

    // Add streaks
    for (int i = 0; i < 5; i++) {
      Sprite sprtFlare = new Sprite(sheet["explosion_flare.png"]);
      sprtFlare.pivot = new Point(0.3, 1.0);
      sprtFlare.scaleX = 0.3;
      sprtFlare.transferMode = sky.TransferMode.plus;
      sprtFlare.rotation = randomDouble() * 360.0;
      addChild(sprtFlare);

      double multiplier = randomDouble() * 0.3 + 1.0;

      Action scale = new ActionTween( (a) => sprtFlare.scaleY = a, 0.3 * multiplier, 0.8, 1.5 * multiplier);
      Action scaleAndRemove = new ActionSequence([scale, new ActionRemoveNode(sprtFlare)]);
      Action fadeIn = new ActionTween( (a) => sprtFlare.opacity = a, 0.0, 1.0, 0.5 * multiplier);
      Action fadeOut = new ActionTween( (a) => sprtFlare.opacity = a, 1.0, 0.0, 1.0 * multiplier);
      Action fadeInOut = new ActionSequence([fadeIn, fadeOut]);
      actions.run(scaleAndRemove);
      actions.run(fadeInOut);
    }
  }
}

class Hud extends Node {
  SpriteSheet sheet;
  Sprite sprtBgScore;

  bool _dirtyScore = true;
  int _score = 0;

  int get score => _score;

  set score(int score) {
    _score = score;
    _dirtyScore = true;
  }

  Hud(this.sheet) {
    position = new Point(310.0, 10.0);
    scale = 0.6;

    sprtBgScore = new Sprite(sheet["scoreboard.png"]);
    sprtBgScore.pivot = new Point(1.0, 0.0);
    sprtBgScore.scale = 0.6;
    addChild(sprtBgScore);
  }

  void update(double dt) {
    // Update score
    if (_dirtyScore) {

      sprtBgScore.removeAllChildren();

      String scoreStr = _score.toString();
      double xPos = -50.0;
      for (int i = scoreStr.length - 1; i >= 0; i--) {
        String numStr = scoreStr.substring(i, i + 1);
        Sprite numSprt = new Sprite(sheet["number_$numStr.png"]);
        numSprt.position = new Point(xPos, 49.0);
        sprtBgScore.addChild(numSprt);
        xPos -= 37.0;
      }
      _dirtyScore = false;
    }
  }
}

class Flash extends NodeWithSize {
  Flash(Size size, this.duration) : super(size) {
    ActionTween fade = new ActionTween((a) => _opacity = a, 1.0, 0.0, duration);
    ActionSequence seq = new ActionSequence([fade, new ActionRemoveNode(this)]);
    actions.run(seq);
  }

  double duration;
  double _opacity = 1.0;
  Paint _cachedPaint = new Paint();

  void paint(PaintingCanvas canvas) {
    // Update the color
    _cachedPaint.color = new Color.fromARGB((255.0 * _opacity).toInt(),
                                            255, 255, 255);
    // Fill the area
    applyTransformForPivot(canvas);
    canvas.drawRect(new Rect.fromLTRB(0.0, 0.0, size.width, size.height),
      _cachedPaint);
  }
}
