import file_system = require('../core/file_system');
import {default as Stats, FileType} from '../core/node_fs_stats';
import {FileFlag, ActionType} from '../core/file_flag';
import {BaseFile, File} from '../core/file';
import {uint8Array2Buffer, buffer2Uint8array} from '../core/util';
import {ApiError, ErrorCode, ErrorStrings} from '../core/api_error';
import {Stats as EmscriptenStats, EmscriptenFSNode} from '../generic/emscripten_fs';

interface EmscriptenError {
  node: EmscriptenFSNode;
  errno: number;
}

function convertError(e: EmscriptenError, path: string = ''): ApiError {
  const errno = e.errno;
  let parent = e.node;
  let paths = [];
  while (parent) {
    paths.unshift(parent.name);
    if (parent === parent.parent) {
      break;
    }
    parent = parent.parent;
  }
  return new ApiError(errno, ErrorStrings[errno], paths.length > 0 ? '/' + paths.join('/') : path);
}

export class EmscriptenFile extends BaseFile implements File {
  constructor(
    private _fs: EmscriptenFileSystem,
    private _FS: any,
    private _path: string,
    private _flag: FileFlag,
    private _stream: any) {
    super();
  }
  public getPos(): number {
    return undefined;
  }
  public close(cb: (err?: ApiError) => void): void {
    let err: ApiError = null;
    try {
      this.closeSync();
    } catch (e) {
      err = e;
    } finally {
      cb(err);
    }
  }
  public closeSync(): void {
    try {
      this._FS.close(this._stream);
    } catch (e) {
      throw convertError(e, this._path);
    }
  }
  public stat(cb: (err: ApiError, stats?: Stats) => any): void {
    try {
      cb(null, this.statSync());
    } catch (e) {
      cb(e);
    }
  }
  public statSync(): Stats {
    try {
      return this._fs.statSync(this._path, false);
    } catch (e) {
      throw convertError(e, this._path);
    }
  }
  public truncate(len: number, cb: (err?: ApiError) => void): void {
    let err: ApiError = null;
    try {
      this.truncateSync(len);
    } catch (e) {
      err = e;
    } finally {
      cb(err);
    }
  }
  public truncateSync(len: number): void {
    try {
      this._FS.ftruncate(this._stream.fd, len);
    } catch (e) {
      throw convertError(e, this._path);
    }
  }
  public write(buffer: NodeBuffer, offset: number, length: number, position: number, cb: (err: ApiError, written?: number, buffer?: NodeBuffer) => any): void {
    try {
      cb(null, this.writeSync(buffer, offset, length, position), buffer);
    } catch (e) {
      cb(e);
    }
  }
  public writeSync(buffer: NodeBuffer, offset: number, length: number, position: number): number {
    try {
      const u8 = buffer2Uint8array(buffer);
      // Emscripten is particular about what position is set to.
      if (position === null) {
        position = undefined;
      }
      return this._FS.write(this._stream, u8, offset, length, position);
    } catch (e) {
      throw convertError(e, this._path);
    }
  }
  public read(buffer: NodeBuffer, offset: number, length: number, position: number, cb: (err: ApiError, bytesRead?: number, buffer?: NodeBuffer) => void): void {
    try {
      cb(null, this.readSync(buffer, offset, length, position), buffer);
    } catch (e) {
      cb(e);
    }
  }
  public readSync(buffer: NodeBuffer, offset: number, length: number, position: number): number {
    try {
      const u8 = buffer2Uint8array(buffer);
      // Emscripten is particular about what position is set to.
      if (position === null) {
        position = undefined;
      }
      return this._FS.read(this._stream, u8, offset, length, position);
    } catch (e) {
      throw convertError(e, this._path);
    }
  }
  public sync(cb: (e?: ApiError) => void): void {
    // NOP.
    cb();
  }
  public syncSync(): void {
    // NOP.
  }
  public chown(uid: number, gid: number, cb: (e?: ApiError) => void): void {
    let err: ApiError = null;
    try {
      this.chownSync(uid, gid);
    } catch (e) {
      err = e;
    } finally {
      cb(err);
    }
  }
  public chownSync(uid: number, gid: number): void {
    try {
      this._FS.fchown(this._stream.fd, uid, gid);
    } catch (e) {
      throw convertError(e, this._path);
    }
  }
  public chmod(mode: number, cb: (e?: ApiError) => void): void {
    let err: ApiError = null;
    try {
      this.chmodSync(mode);
    } catch (e) {
      err = e;
    } finally {
      cb(err);
    }
  }
  public chmodSync(mode: number): void {
    try {
      this._FS.fchmod(this._stream.fd, mode);
    } catch (e) {
      throw convertError(e, this._path);
    }
  }
  public utimes(atime: Date, mtime: Date, cb: (e?: ApiError) => void): void {
    let err: ApiError = null;
    try {
      this.utimesSync(atime, mtime);
    } catch (e) {
      err = e;
    } finally {
      cb(err);
    }
  }
  public utimesSync(atime: Date, mtime: Date): void {
    this._fs.utimesSync(this._path, atime, mtime);
  }
}

/**
 * A simple in-memory file system backed by an InMemoryStore.
 */
export default class EmscriptenFileSystem extends file_system.SynchronousFileSystem {
  private _FS: any;

  constructor(_FS: any) {
    super();
    this._FS = _FS;
  }

  public static isAvailable(): boolean { return true; }
  public getName(): string { return this._FS.DB_NAME(); }
  public isReadOnly(): boolean { return false; }
  public supportsLinks(): boolean { return true; }
  public supportsProps(): boolean { return true; }
  public supportsSynch(): boolean { return true; }

  public renameSync(oldPath: string, newPath: string): void {
    try {
      this._FS.rename(oldPath, newPath);
    } catch (e) {
      if (e.errno === ErrorCode.ENOENT) {
        throw convertError(e, this.existsSync(oldPath) ? newPath : oldPath);
      } else {
        throw convertError(e);
      }
    }
  }

  public statSync(p: string, isLstat: boolean): Stats {
    try {
      const stats = isLstat ? this._FS.lstat(p) : this._FS.stat(p);
      const item_type = this.modeToFileType(stats.mode);
      return new Stats(
        item_type,
        stats.size,
        stats.mode,
        stats.atime,
        stats.mtime,
        stats.ctime
      );
    } catch (e) {
      throw convertError(e, p);
    }
  }

  private modeToFileType(mode: number): FileType {
    if (this._FS.isDir(mode)) {
      return FileType.DIRECTORY;
    } else if (this._FS.isFile(mode)) {
      return FileType.FILE;
    } else if (this._FS.isLink(mode)) {
      return FileType.SYMLINK;
    }
  }

  /**
   * Moved to separate function to avoid perf hit.
   */
  private _tryStats(p: string): Stats {
    try {
      return this.statSync(p, false);
    } catch (e) {
      return null;
    }
  }

  public openSync(p: string, flag: FileFlag, mode: number): EmscriptenFile {
    try {
      const stream = this._FS.open(p, flag.getFlagString(), mode);
      if (this._FS.isDir(stream.node.mode)) {
        this._FS.close(stream);
        throw ApiError.EISDIR(p);
      }
      return new EmscriptenFile(this, this._FS, p, flag, stream);
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public unlinkSync(p: string): void {
    try {
      this._FS.unlink(p);
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public rmdirSync(p: string): void {
    try {
      this._FS.rmdir(p);
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public mkdirSync(p: string, mode: number): void {
    try {
      this._FS.mkdir(p, mode);
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public readdirSync(p: string): string[] {
    try {
      // Emscripten returns items for '.' and '..'. Node does not.
      return this._FS.readdir(p).filter((p) => p !== '.' && p !== '..');
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public truncateSync(p: string, len: number): void {
    try {
      this._FS.truncate(p, len);
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public readFileSync(p: string, encoding: string, flag: FileFlag): any {
    try {
      const data: Uint8Array = this._FS.readFile(p, { flags: flag.getFlagString() })
      const buff = uint8Array2Buffer(data);
      if (encoding) {
        return buff.toString(encoding);
      } else {
        return buff;
      }
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public writeFileSync(p: string, data: any, encoding: string, flag: FileFlag, mode: number): void {
    try {
      if (encoding) {
        data = new Buffer(data, encoding);
      }
      const u8 = buffer2Uint8array(data);
      this._FS.writeFile(p, u8, { flags: flag.getFlagString(), encoding: 'binary' });
      this._FS.chmod(p, mode);
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public chmodSync(p: string, isLchmod: boolean, mode: number) {
    try {
      isLchmod ? this._FS.lchmod(p, mode) : this._FS.chmod(p, mode);
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public chownSync(p: string, isLchown: boolean, uid: number, gid: number): void {
    try {
      isLchown ? this._FS.lchown(p, uid, gid) : this._FS.chown(p, uid, gid);
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public symlinkSync(srcpath: string, dstpath: string, type: string): void {
    try {
      this._FS.symlink(srcpath, dstpath);
    } catch (e) {
      throw convertError(e);
    }
  }

  public readlinkSync(p: string): string {
    try {
      return this._FS.readlink(p);
    } catch (e) {
      throw convertError(e, p);
    }
  }

  public utimesSync(p: string, atime: Date, mtime: Date): void {
    try {
      this._FS.utime(p, atime.getTime(), mtime.getTime());
    } catch (e) {
      throw convertError(e, p);
    }
  }

}
