import { Parser } from './parse/stream';
import { Through } from '../through';
import { Stream, Transform, TransformOptions } from 'stream';
import { basename } from 'path/posix';

class Parser2 extends Parser {
  stream: Through;
  root?: any;
  header?: any;
  footer?: any;
  count = 0;

  setHeaderFooter(key: string, value: string) {
    // header has not been emitted yet
    if (this.header !== false) {
      this.header = this.header || {}
      this.header[key] = value
    }

    // footer has not been emitted yet but header has
    if (this.footer !== false && this.header === false) {
      this.footer = this.footer || {}
      this.footer[key] = value
    }
  }

  constructor(public path: string[], public map: any) {
    super();

    this.stream = new Through(this.transform, this.flush);
  }

  transform(chunk: any, encoding: BufferEncoding, callback: (error?: Error, data?: any) => void): void {
    if (typeof chunk === 'string') chunk = Buffer.from(chunk)
    this.write(chunk);
    callback();
  }

  flush(callback: (error?: Error, data?: any) => void): void {
    if (this.header) {
      this.stream.emit('header', this.header);
    }
    if (this.footer) {
      this.stream.emit('footer', this.footer);
    }

    if (this.tState != Parser.C.START || this.stack.length > 0) {
      callback(new Error('Incomplete JSON'))
      return
    }
    callback()
  }

  onValue(value: any) {
    if (!this.root) {
      this.stream.root = value
    }

    if (!this.path) {
      return;
    }

    let i = 0 // iterates on path
    let j = 0 // iterates on stack
    let emitKey = false
    let emitPath = false
    while (i < this.path.length) {
      let key: any = this.path[i]
      let c
      j++

      if (key && !key.recurse) {
        c = j === this.stack.length ? this : this.stack[j]
        if (!c) return
        if (!check(key, c.key)) {
          this.setHeaderFooter(c.key, value)
          return
        }
        emitKey = !!key.emitKey
        emitPath = !!key.emitPath
        i++
      } else {
        i++
        let nextKey = this.path[i]
        if (!nextKey) return
        while (true) {
          c = j === this.stack.length ? this : this.stack[j]
          if (!c) return
          if (check(nextKey, c.key)) {
            i++
            if (!Object.isFrozen(this.stack[j])) this.stack[j].value = null
            break
          } else {
            this.setHeaderFooter(c.key, value)
          }
          j++
        }
      }
    }

    // emit header
    if (this.header) {
      this.stream.emit('header', this.header)
      this.header = false
    }
    if (j !== this.stack.length) return

    this.count++
    let actualPath = this.stack
      .slice(1)
      .map(function (element: any) {
        return element.key
      })
      .concat([this.key])
    let data = value
    if (null != data)
      if (null != (data = this.map ? this.map(data, actualPath) : data)) {
        if (emitKey || emitPath) {
          data = { value: data }
          if (emitKey) data['key'] = this.key
          if (emitPath) data['path'] = actualPath
        }

        this.stream.push(data)
      }
    if (this.value) delete this.value[this.key]
    for (let k in this.stack) if (!Object.isFrozen(this.stack[k])) this.stack[k].value = null
  }

  _onToken = super.onToken

  onToken(token: any, value: any) {
    super.onToken(token, value)
    if (this.stack.length === 0) {
      if (this.stream.root) {
        if (!this.path) {
          this.stream.push(this.stream.root)
        }
        this.count = 0
        this.stream.root = null
      }
    }
  }

  onError(err: any) {
    if (err.message.indexOf('at position') > -1) err.message = 'Invalid JSON (' + err.message + ')'
    this.stream.destroy(err)
  }

}

export function parse(path: any, map: any): any {
  if ('string' === typeof path)
    path = path.split('.').map(function (e) {
      if (e === '$*') return { emitKey: true }
      else if (e === '*') return true
      else if (e === '')
        // '..'.split('.') returns an empty string
        return { recurse: true }
      else return e
    });

  if (!path || !path.length) {
    path = null
  }

  if (!path || !path.length) path = null

  const parser = new Parser2(path, map);

  return parser.stream;
}

function check(x: any, y: any): any {
  if ('string' === typeof x) return y == x
  else if (x && 'function' === typeof x.exec) return x.exec(y)
  else if ('boolean' === typeof x || 'object' === typeof x) return x
  else if ('function' === typeof x) return x(y)
  return false
}

class JsonStream extends Through implements ReadableStream {
  constructor(
    transform: (chunk: any, encoding: BufferEncoding, callback: (error?: Error, data?: any) => void) => void,
    flush: (callback: (error?: Error, data?: any) => void) => void,
    options?: TransformOptions) {
    super(transform, flush, options);
    this.locked = false;
  }

  locked: boolean;
  
  cancel(reason?: any): Promise<void> {
    throw new Error('Method not implemented.');
  }
  getReader(): ReadableStreamDefaultReader<any> {
    throw new Error('Method not implemented.');
  }
  pipeThrough<T>(transform: ReadableWritablePair<T, any>, options?: StreamPipeOptions): ReadableStream<T> {
    throw new Error('Method not implemented.');
  }
  pipeTo(destination: WritableStream<any>, options?: StreamPipeOptions): Promise<void> {
    throw new Error('Method not implemented.');
  }
  tee(): [ReadableStream<any>, ReadableStream<any>] {
    throw new Error('Method not implemented.');
  }
  forEach(callbackfn: (value: any, key: number, parent: ReadableStream<any>) => void, thisArg?: any): void {
    throw new Error('Method not implemented.');
  }
}

class Serializer {
  stream: JsonStream;
  first = true;
  anyData = false;

  constructor(public op?: any, public sep?: any, public cl?: any, public indent?: number) {
    this.indent = indent || 0
    if (op === false) {
      this.op = ''
      this.sep = '\n'
      this.cl = ''
    } else if (op == null) {
      this.op = '[\n'
      this.sep = '\n,\n'
      this.cl = '\n]\n'
    }

    this.stream = new JsonStream(this.transform, this.flush);
  }

  transform(data: any, _: any, cb: any): void {
    this.anyData = true
    let json
    try {
      json = JSON.stringify(data, null, this.indent)
    } catch (err) {
      return cb(err)
    }
    if (this.first) {
      this.first = false
      cb(null, (typeof this.op === 'function' ? this.op() : this.op) + json)
    } else {
      cb(null, this.sep + json)
    }
  }

  flush(cb: any) {
    if (!this.anyData) this.stream.push(this.op)
    if (typeof this.cl === 'function') {
      this.cl((err: any, res: any) => {
        if (err) {
          return cb(err)
        }
        this.stream.push(res);
        cb();
      })
      return;
    }
    this.stream.push(this.cl);
    cb();
  }
}

export function stringify(op?: any, sep?: any, cl?: any, indent?: number): ReadableStream {
  const serializer = new Serializer(op, sep, cl, indent);

  return serializer.stream;
}

class ObjectSerializer {
  stream: Through;
  first = true;
  anyData = false;

  constructor(public op?: any, public sep?: any, public cl?: any, public indent?: number) {
    this.indent = indent || 0
    if (op === false) {
      this.op = '';
      this.sep = '\n';
      this.cl = '';
    } else if (op == null) {
      this.op = '{\n';
      this.sep = '\n,\n';
      this.cl = '\n}\n';
    }

    this.stream = new Through(this.transform, this.flush);
  }

  transform(data: any, enc: any, cb: any): void {
    this.anyData = true
    let json
    try {
      json = JSON.stringify(data[0]) + ':' + JSON.stringify(data[1], null, this.indent)
    } catch (err) {
      return cb(err)
    }
    if (this.first) {
      this.first = false
      cb(null, (typeof this.op === 'function' ? this.op() : this.op) + json)
    } else {
      cb(null, this.sep + json)
    }
  }

  flush(cb: any) {
    if (!this.anyData) this.stream.push(this.op)
    if (typeof this.cl === 'function') {
      this.cl((err: any, res: any) => {
        if (err) {
          return cb(err);
        }
        this.stream.push(res);
        cb();
      })
      return;
    }
    this.stream.push(this.cl);
    cb()
  }
}

export function stringifyObject(op?: any, sep?: any, cl?: any, indent?: number) {
  const serializer = new ObjectSerializer(op, sep, cl, indent);
  return serializer.stream;
}