import tango.io.Console;
import tango.io.Stdout;
import tango.io.stream.Data;
import tango.util.container.LinkedList;

import Integer = tango.text.convert.Integer;
import tango.core.Exception;

import tango.math.Math;
import tango.math.random.Kiss;

static Kiss *rand;
static int width;
static int height;

uint[] makePalette() {
  auto palette = new uint[256];
  auto aR = 1F;
  auto aG = 1F / 255 / 255;
  for (auto i = 0; i < palette.length; i++) {
    auto r = cast(int)(aR * i);
    auto g = cast(int)(aG * i * i * i);
    auto b = 0; // cast(int)(max(128 - i * 5, 0));
    palette[i] = 0xFF000000 | b << 16 | g << 8 | r;
  }
  return palette;
}

class Canvas {
  int width;
  int height;
  ubyte[] canvas;
  uint[] palette;
  float decayFactor;

  this(int height, int width) {
    this.width = width;
    this.height = height;
    this.canvas = new ubyte[width * height];
    this.palette = makePalette();
    decayFactor = 0.99;
  }

  void render(int[] texture) {
    for (int i = 0; i < canvas.length; i++)
      texture[i] = palette[canvas[i]];
  }

  ubyte[] getLine(int line) {
    auto x0 = line * width;
    return canvas[x0 .. x0 + width];
  }

  void setPixel(int x, int y, ubyte value) {
    canvas[width * y + x] = value;
  }

  void setPixelMax(int x, int y, ubyte value) {
    auto loc = width * y + x;
    canvas[loc] = max(canvas[loc], value);
  }

  void diffuse() {
    for (int y = 0; y < height - 1; y++) {
      auto next = getLine(y);
      auto prev1 = getLine(y + 1);
      auto prev2 = getLine(y + 1);
      next[0] = cast(ubyte)((next[0] + 2*prev1[0] + prev1[1]) / 4 * decayFactor);
      for (int i = 1; i < width - 1; i++)
        next[i] = cast(ubyte)((next[i] + prev1[i-1] + 2*prev1[i] + prev1[i+1]) / 5 * decayFactor);
      auto r = next.length - 1;
      next[r] = cast(ubyte)((next[r] + 2*prev1[r] + prev1[r-1]) / 4 * decayFactor);
    }
  }
}

ubyte getBrightness() {
  return 63 + 64 * rand.natural(4);
}

class Drop {
  static const float G = .06; // Gravity
  static const float R = .98; // "Air resistance"
  static const auto lifeExp = 10; // Inverse probability of drop disappearing
  static const auto appearanceExp = .2; // Inverse probability of drop appearing

  float x;
  float vx;
  float y;
  float vy;

  this(int x, int y, float vx, float vy) {
    this.x = x;
    this.y = y;
    this.vx = vx;
    this.vy = vy;
  }

  this(int x, int y) {
    this.x = x;
    this.vx = 0;
    this.y = y;
    this.vy = 0.7;
  }

  void update() {
    x += vx;
    y += vy;
    vy += G;
    vx *= R;
    vy *= R;
  }

  void draw(Canvas *canvas) {
    int ix = cast(int)(x);
    int iy = cast(int)(y);

    canvas.setPixelMax(ix, iy, 255);
    canvas.setPixelMax(ix - 1, iy, getBrightness());
    canvas.setPixelMax(ix + 1, iy, getBrightness());
    canvas.setPixelMax(ix, iy + 1, 240); // WTF, getBrightness segfaults here
  }
}

void flameLine(Canvas canvas, int x0, int y0, int x1, int y1) {
  // No Bresenham this time
  if (abs(x0 - x1) < abs(y0 - y1)) {
    // This is steep
    if (y0 > y1) {
      int tmp = y1;
      y1 = y0;
      y0 = tmp;

      tmp = x1;
      x1 = x0;
      x0 = tmp;
    }

    auto slope = cast(double)(x1 - x0) / (y1 - y0);
    for (int y = y0; y <= y1; y++) {
      ubyte brightness = 64;
      if (rand.natural(2) > 0)
        brightness = 255;
      canvas.setPixelMax(cast(int)(x0 + slope * (y - y0)), y, brightness);
    }
  } else {
    // Not steep
    if (x0 > x1) {
      int tmp = y1;
      y1 = y0;
      y0 = tmp;

      tmp = x1;
      x1 = x0;
      x0 = tmp;
    }

    auto slope = cast(double)(y1 - y0) / (x1 - x0);
    for (int x = x0; x <= x1; x++) {
      canvas.setPixelMax(x, cast(int)(y0 + slope * (x - x0)), getBrightness());
    }
  }
}

void updateDrops(LinkedList!(Drop) *drops, Canvas *canvas) {
  auto iterator = drops.iterator();
  Drop *drop;
  while ((drop = iterator.next()) != null) {
    drop.update();
    if (drop.y > height - 2 || rand.natural(Drop.lifeExp) < 1 || drop.y < 0
        || drop.x < 0 || drop.x >= width)
      iterator.remove();
    else 
      drop.draw(canvas);
  }
}

void main(char[][] argv) {
  if (argv.length != 3) {
    Cerr("This is a process wrapper plugin. Use correct arguments. Console will probably");
    Cerr.newline;
    Cerr("break. You have been warned.").newline;
    return;
  }

  width = Integer.parse(argv[1]);
  height = Integer.parse(argv[2]);

  auto canvas = new Canvas(width, height);

  auto texSize = width * height;
  auto tex = new int[texSize];

  auto input = new DataInput(Cin.stream);

  // init rand
  rand = new Kiss();

  LinkedList!(Drop) drops = new LinkedList!(Drop)();

  auto speed = 2 * PI / 20000;
  // auto speed = 2 * PI / 2000; // For great silliness
  try {
    while(true) {
      auto timeMs = input.int32;
      auto phase = timeMs * speed;

      auto cx = width / 2;
      auto cy = height / 2;
      auto r = 0.9 * min(cx, cy);

      for (int i = 0; i < 5; i++) {
        auto nextPhase = phase + 4 * PI / 5;
        auto startX = cast(int)(cx + r * sin(phase));
        auto startY = cast(int)(cy + r * cos(phase));
        auto endX = cast(int)(cx + r * sin(nextPhase));
        auto endY = cast(int)(cy + r * cos(nextPhase));
        flameLine(canvas, startX, startY, endX, endY);

        if (rand.fraction * Drop.appearanceExp * 5 < 1) {
            auto fudgeFactor = 10;
            auto startVx = r * cos(phase) * speed * fudgeFactor;
            auto startVy = r * -sin(phase) * speed * fudgeFactor;
            auto endVx = r * cos(nextPhase) * speed * fudgeFactor;
            auto endVy = r * -sin(nextPhase) * speed * fudgeFactor;

            auto loc = rand.fraction();
            drops.add(new Drop(cast(int)(loc * startX + (1 - loc) * endX),
                               cast(int)(loc * startY + (1 - loc) * endY),
                               loc * startVx + (1 - loc) * endVx,
                               loc * startVy + (1 - loc) * endVy));
        }

        phase = nextPhase;
      }

      updateDrops(&drops, &canvas);

      canvas.diffuse;
      canvas.render(tex);
      Cout.stream.write(tex);
    }
  } catch (IOException e) {
    Cerr("Caught IOException, exiting").newline;
  }
}