import * as fs from 'fs';
import * as Stream from 'stream';
import * as Events from 'events';

import * as JS from '../lib/jsonstream';

// Create a chunkier stream to test whether client deals with all chunk boundaries properly
class ChunkedStream extends JS.Readable
{
  s: Stream.Readable;
  chunkSize: number;
  bDone: boolean;

  constructor(s: Stream.Readable, chunkSize: number = 100)
    {
      super();
      this.s = s;
      this.chunkSize = chunkSize;
      this.bDone = false;
      this.onEnd = this.onEnd.bind(this);
      this.onError = this.onError.bind(this);
      this.onClose = this.onClose.bind(this);
      this.onData = this.onData.bind(this);
      this.s.on('end', this.onEnd);
      this.s.on('error', this.onError);
      this.s.on('close', this.onClose);
      this.s.on('data', this.onData);
    }

  onData(chunk: Buffer): void
    {
      let offset: number = 0;
      let len: number = chunk.length;

      while (!this.bDone && offset < len)
      {
        let n: number = len - offset;
        if (n > this.chunkSize)
          n = this.chunkSize;
        let subChunk: Buffer = chunk.slice(offset, offset+n);
        this.emit('data', subChunk);
        offset += n;
      }
    }

  onEnd(): void
    {
      this.bDone = true;
      this.emit('end');
    }

  onError(e: Error): void
    {
      this.bDone = true;
      this.emit('error', e);
    }

  onClose(): void
    {
      this.bDone = true;
      this.emit('close');
    }
}

// Create a stream with lots of back pressure to validate client deals well with it
class GlitchyStream extends JS.Writable
{
  s: Stream.Writable;
  chunkSize: number;
  buf: Buffer;
  bufLen: number;
  bDone: boolean;
  bPaused: boolean;

  constructor(s: Stream.Writable, chunkSize: number = 10000)
    {
      super();
      this.s = s;
      this.chunkSize = chunkSize;
      this.buf = Buffer.alloc(chunkSize);
      this.bufLen = 0;
      this.bDone = false;
      this.bPaused = false;
      this.onFinish = this.onFinish.bind(this);
      this.onError = this.onError.bind(this);
      this.onClose = this.onClose.bind(this);
      this.onDrain = this.onDrain.bind(this);
      this.s.on('finish', this.onFinish);
      this.s.on('error', this.onError);
      this.s.on('close', this.onClose);
      this.s.on('drain', this.onDrain);
    }

  write(a: any, encoding?: string, cb?: () => void): boolean
    {
      if (this.bDone)
        throw 'GlitchyStream: write called when stream done. Huh?';
      if (this.bPaused)
        throw 'GlitchyStream: write called while stream paused. Wait for drain event.';
      if (a.length > this.chunkSize)
        throw 'GlitchyStream: write called with chunk too big.';

      // If I can fit it in my small chunk, do so.
      if (a.length + this.bufLen <= this.chunkSize)
      {
        a.copy(this.buf, this.bufLen);
        this.bufLen += a.length;
        return true;
      }
      else
      {
        let bHere: number = this.chunkSize - this.bufLen;
        let bLeft: number = a.length - bHere;
        a.copy(this.buf, this.bufLen, 0, bHere);
        let bOK: boolean = this.s.write(this.buf);
        a.copy(this.buf, 0, bHere);
        this.bufLen = bLeft;

        // If underlying stream successfully wrote, it won't generate a drain event so I need to force that.
        if (bOK)
          process.nextTick(this.onDrain);

        // Create backpressure when chunk overflows
        return false;
      }
    }

  end(): void
    {
      if (!this.bDone && this.bufLen > 0)
      {
        let b = this.buf.slice(0, this.bufLen);
        this.s.write(b);
        this.bufLen = 0;
      }
      this.bDone = true;
    }

  onDrain(): void
    {
      // called by underlying stream as well as by my own implementation. Just forward.
      this.emit('drain');
    }

  onFinish(): void
    {
      this.bDone = true;
      this.emit('finish');
    }

  onError(e: Error): void
    {
      this.bDone = true;
      this.emit('error', e);
    }

  onClose(): void
    {
      this.bDone = true;
      this.emit('close');
    }
}

let file: string = process.argv[process.argv.length - 1];

let jsonReadStream: JS.JSONStreamReader = new JS.JSONStreamReader({ syncChunkSize: 10 });
let jsonWriteStream: JS.JSONStreamWriter = new JS.JSONStreamWriter();
jsonReadStream.on('end', (o: any) => { jsonWriteStream.start(o, new GlitchyStream(process.stdout)); });
jsonReadStream.on('error', (err: Error) => { console.log(err.message); });
//jsonReadStream.start(new ChunkedStream(fs.createReadStream(file)));
let bufferReadable = new JS.BufferReadable(fs.readFileSync(file));
jsonReadStream.start(bufferReadable);
