'use strict';
const fs = require('fs-ext');
const os = require('os');
const sleep = require('sleep');

const tty = require('tty');

const Hash = require('./hash.js');

const core = require('./core.js');

const hll = require('./hll.js');

const child_process = require('child_process');

const NQPObject = require('./nqp-object.js');

const mkdirp = require('mkdirp');

const NQPException = require('./nqp-exception.js');

const nqp = require('./runtime.js');

const Null = require('./null.js');

const NQPInt = require('./nqp-int.js');
const NQPStr = require('./nqp-str.js');

const BOOT = require('./BOOT.js');

function boolish(bool) {
  return bool ? 1 : 0;
}

const op = {};
exports.op = op;

op.print = function(arg) {
  process.stdout.write(arg);
  return arg;
};

op.say = function(arg) {
  process.stdout.write(arg + '\n');
  return arg;
};

function isFile(path, mode) {
  try {
    fs.accessSync(path, mode);
  } catch (e) {
    return 0;
  }
  return 1;
}

op.filewritable = function(path) {
  return isFile(path, fs.constants.W_OK);
};

op.fileexecutable = function(path) {
  return isFile(path, fs.constants.X_OK);
};

op.filereadable = function(path) {
  return isFile(path, fs.constants.R_OK);
};

function stat(file, code, lstat) {
  const EXISTS = 0;
  const FILESIZE = 1;
  const ISDIR = 2;
  const ISREG = 3;
  const ISDEV = 4;
  const CREATETIME = 5;
  const ACCESSTIME = 6;
  const MODIFYTIME = 7;
  const CHANGETIME = 8;
  const BACKUPTIME = 9;
  const UID = 10;
  const GID = 11;
  const ISLNK = 12;
  const PLATFORM_DEV = -1;
  const PLATFORM_INODE = -2;
  const PLATFORM_MODE = -3;
  const PLATFORM_NLINKS = -4;
  const PLATFORM_DEVTYPE = -5;
  const PLATFORM_BLOCKSIZE = -6;
  const PLATFORM_BLOCKS = -7;

  // we can't use fs.existsSync(file) as it follows symlinks
  let stats;
  try {
    if (lstat || code == ISLNK) {
      stats = fs.lstatSync(file);
    } else {
      stats = fs.statSync(file);
    }
  } catch (err) {
    if (code == EXISTS && err.code === 'ENOENT') {
      return 0;
    } else {
      throw err;
    }
  }

  switch (code) {
    case EXISTS: return 1;
    case FILESIZE: return stats.size;
    case ISDIR: return boolish(stats.isDirectory());
    case ISREG: return boolish(stats.isFile());
    case ISDEV:
      return boolish(stats.isCharacterDevice() || stats.isBlockDevice());
    case CREATETIME: return -1;
    case ACCESSTIME: return stats.atime.getTime() / 1000;
    case MODIFYTIME: return stats.mtime.getTime() / 1000;
    case CHANGETIME: return stats.ctime.getTime() / 1000;
    case BACKUPTIME: return -1;
    case UID: return stats.uid;
    case GID: return stats.gid;
    case ISLNK: return boolish(stats.isSymbolicLink());
    case PLATFORM_DEV: return stats.dev;
    case PLATFORM_INODE: return stats.ino;
    case PLATFORM_MODE: return stats.mode;
    case PLATFORM_NLINKS: return stats.nlink;
    case PLATFORM_DEVTYPE: return stats.rdev;
    case PLATFORM_BLOCKSIZE: return stats.blksize;
    case PLATFORM_BLOCKS: return stats.blocks;
  }
}

op.fileislink = function(file) {
  return boolish(fs.lstatSync(file).isSymbolicLink());
};

op.stat = function(file, code) {
  return stat(file, code, false) | 0;
};

op.stat_time = function(file, code) {
  return stat(file, code, false);
};

op.lstat = function(file, code) {
  return stat(file, code, true) | 0;
};

op.lstat_time = function(file, code) {
  return stat(file, code, true);
};

class IOHandle extends NQPObject {
  $$can(ctx, name) {
    return 0;
  }

  $$toBool(ctx) {
    return 1;
  }

  $$serializeAsNull() {
    return 1;
  }

  $$setinputlinesep(sep) {
    this.seps = [sep];
  }

  $$setinputlineseps(seps) {
    this.seps = seps.array;
  }

  $$setbuffersizefh(size) {
    return this;
  }
};

class FileHandle extends IOHandle {
  constructor(fd) {
    super();
    this.fd = fd;
    this.eof = false;
  }

  $$closefh() {
    fs.closeSync(this.fd);
    return this;
  }

  $$isttyfh() {
    return tty.isatty(this.fd) ? 1 : 0;
  }


  $$toBool(ctx) {
    return 1;
  }

  $$filenofh() {
    return this.fd;
  }

  $$eoffh() {
    return this.eof ? 1 : 0;
  }

  $$tellfh() {
    return fs.seekSync(this.fd, 0, 1);
  }

  $$lockfh(fh, flags) {
    fs.flockSync(this.fd, (flags & 0x01 ? 'sh' : 'ex') + (flags & 0x10 ? 'nb' : ''));
    return this;
  }

  $$unlockfh(flags) {
    fs.flockSync(this.fd, 'un');
    return this;
  }

  $$flushfh() {
    // STUB - will require custom io to implement
    return this;
  }

  $$writefh(buf) {
    const buffer = core.toRawBuffer(buf);
    return fs.writeSync(this.fd, buffer, 0, buffer.length);
  }

  $$readfh(buf, bytes) {
    const isUnsigned = buf.$$STable.REPR.type.$$STable.REPR.isUnsigned;
    const buffer = Buffer.allocUnsafe(bytes);
    const read = fs.readSync(this.fd, buffer, 0, bytes, null);
    if (read === 0) {
      this.eof = true;
    }
    buf.array.length = read;
    for (let i = 0; i < read; i++) {
      if (isUnsigned) {
        buf.array[i] = buffer[i];
      } else {
        buf.array[i] = buffer[i] > 127 ? buffer[i] - 256 : buffer[i];
      }
    }
    return buf;
  }
};

exports.FileHandle = FileHandle;

function modeToFlags(mode) {
  let flags;

  if (mode === 'w') {
    flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC;
  } else {
    if (mode[0] === 'r') flags = fs.constants.O_RDONLY;
    else if (mode[0] === '-') flags = fs.constants.O_WRONLY;
    else if (mode[0] === '+') flags = fs.constants.O_RDWR;
    else if (mode[0] === 'w') flags = fs.constants.O_WRONLY | fs.constants.O_CREAT;
    else throw 'unknown mode to open: ' + mode;
  }

  for (const c of mode.substr(1)) {
    if (c === 'a') flags |= fs.constants.O_APPEND;
    else if (c === 'c') flags |= fs.constants.O_CREAT;
    else if (c === 't') flags |= fs.constants.O_TRUNC;
    else if (c === 'x') flags |= fs.constants.O_EXCL;
    else throw 'unknown mode to open: ' + mode;
  }

  return flags;
}

op.open = function(name, mode) {
  try {
    const fh = new FileHandle(fs.openSync(name, modeToFlags(mode)));
    return fh;
  } catch (e) {
    if (e.code === 'ENOENT') {
      throw new NQPException(`Failed to open file ${name}: No such file or director`);
    } else {
      throw e;
    }
  }
};

op.seekfh = function(ctx, fh, offset, whence) {
  if (whence == 0 && offset < 0) {
    throw new NQPException(`Can't seek to position: ${offset}`);
  }
  if (!(whence == 0 || whence == 1 || whence == 2)) {
    throw new NQPException('Invalid whence passed to seekfh: ' + whence);
  }
  return fs.seekSync(fh.fd, offset, whence);
};

op.unlink = function(filename) {
  try {
    fs.unlinkSync(filename);
  } catch (e) {
    if (e.code !== 'ENOENT') {
      throw e;
    }
  }
};

op.link = function(srcpath, dstpath) {
  fs.linkSync(srcpath, dstpath);
};

op.symlink = function(srcpath, dstpath) {
  fs.symlinkSync(srcpath, dstpath);
};

op.chdir = function(dir) {
  process.chdir(dir);
};

op.rmdir = function(dir) {
  try {
    fs.rmdirSync(dir);
  } catch (err) {
    if (err.code === 'ENOENT') {
    } else if (err.code === 'ENOTEMPTY') {
      throw new NQPException('Failed to rmdir: directory not empty');
    } else {
      throw err;
    }
  }
};

op.mkdir = function(dir, mode) {
  mkdirp.sync(dir, mode);
};

class DirHandle {
  constructor(contents) {
    this.contents = contents;
    this.idx = 0;
  }

  $$nextfiledir() {
    if (this.idx === this.contents.length) return '';
    return this.contents[this.idx++];
  }

  $$decont(ctx) {
    return this;
  }

  $$closedir() {
  }

  $$istype(type) {
    return 0;
  }

  $$toBool(ctx) {
    return 1;
  }
};

op.opendir = function(path) {
  return new DirHandle(fs.readdirSync(path));
};

op.cwd = function() {
  return process.cwd();
};

op.getenvhash = function() {
  const hash = new Hash();
  for (const key in process.env) {
    hash.content.set(key, new NQPStr(process.env[key]));
  }
  return hash;
};

class StdHandle extends IOHandle {
  constructor() {
    super();
  }

  $$flushfh() {
    return this;
  }
};

class Stderr extends StdHandle {
  constructor() {
    super();
  }

  $$filenofh() {
    return process.stderr.fd;
  }

  $$isttyfh() {
    return (process.stderr.isTTY ? 1 : 0);
  }

  $$writefh(buf) {
    const buffer = core.toRawBuffer(buf);
    process.stderr.write(buffer);
  }
};

const STDERR = new Stderr();
op.getstderr = function() {
  return STDERR;
};

class Stdout extends StdHandle {
  constructor() {
    super();
    this.tellCount = 0;
  }

  $$isttyfh() {
    return (process.stdout.isTTY ? 1 : 0);
  }

  $$filenofh() {
    return process.stdout.fd;
  }

  $$flushfh() {
  }

  $$writefh(buf) {
    const buffer = core.toRawBuffer(buf);
    this.tellCount += buffer.length;
    process.stdout.write(buffer);
  }

  /* HACK - needed for the REPL to run */
  $$tellfh() {
    return this.tellCount;
  }
};

const STDOUT = new Stdout();
op.getstdout = function() {
  return STDOUT;
};


let stdin;
op.getstdin = function() {
  if (!stdin) {
    let fd;
    try {
      fd = fs.openSync('/dev/stdin', 'rs');
    } catch (e) {
      /* this should work on Windows, we need to test it tho */
      fd = 0;
    };
    stdin = new FileHandle(fd);
  }

  return stdin;
};

op.exit = function(code) {
  process.exit(code);
};

op.readlink = function(path) {
  return fs.readlinkSync(path);
};

op.gethostname = function() {
  return os.hostname();
};

op.chmod = function(path, mode) {
  return fs.chmodSync(path, mode);
};

op.chown = function(path, uid, gid) {
  return fs.chownSync(path, uid, gid);
};

op.sleep = function(seconds) {
  sleep.usleep(Math.floor(seconds * 1000000));
  return seconds;
};

op.copy = function(source, target) {
  fs.writeFileSync(target, fs.readFileSync(source));
};

op.rename = function(oldPath, newPath) {
  fs.renameSync(oldPath, newPath);
};

function wrapBuffer(buffer, type) {
  const elementSize = core.byteSize(type);

  const isUnsigned = type.$$STable.REPR.type.$$STable.REPR.isUnsigned;

  const wrapped = type.$$STable.REPR.allocate(type.$$STable);

  let offset = 0;
  for (let i = 0; i < buffer.length / elementSize; i++) {
    wrapped.array[i] = isUnsigned ? buffer.readUIntLE(offset, elementSize) : buffer.readIntLE(offset, elementSize);
    offset += elementSize;
  }

  return wrapped;
}

/*async*/ function stringifyEnv(ctx, hash) {
  const stringifed = {};

  for (const key of hash.content.keys()) {
    stringifed[key] = /*await*/ nqp.toStr(hash.content.get(key), ctx);
  }

  return stringifed;
}

/*async*/ function stringifyArray(ctx, array) {
  const stringified = [];
  for (const element of array.array) {
    stringified.push(/*await*/ nqp.toStr(element, ctx));
  }
  return stringified;
}

op.spawnprocasync = /*async*/ function(ctx, queue, prog, args, cwd, env, config) {
  const stringified = /*await*/ stringifyArray(ctx, args);
  const options = {
    shell: false,
    windowsVerbatimArguments: true,
    argv0: stringified.shift(),
    cwd: cwd,
    env: /*await*/ stringifyEnv(ctx, env),
    stdio: [
      process.stdin,
      config.content.get('stdout_bytes') ? 'pipe' : process.stdout,
      config.content.get('stderr_bytes') ? 'pipe' : process.stderr,
    ],
  };

  const result = child_process.spawnSync(prog, stringified, options);

  if (config.content.get('ready')) {
    /*await*/ config.content.get('ready').$$call(ctx, null);
  }


  let str_box = ctx.$$getHLL().get('str_box');

  if (str_box === undefined) str_box = Null;

  if (config.content.get('stdout_bytes')) {
    /*await*/ config.content.get('stdout_bytes').$$call(ctx, null, new NQPInt(0), wrapBuffer(result.output[1], config.content.get('buf_type')), str_box);
    /*await*/ config.content.get('stdout_bytes').$$call(ctx, null, new NQPInt(1), str_box, str_box);
  }

  if (config.content.get('stderr_bytes')) {
    /*await*/ config.content.get('stderr_bytes').$$call(ctx, null, new NQPInt(0), wrapBuffer(result.output[2], config.content.get('buf_type')), str_box);
    /*await*/ config.content.get('stderr_bytes').$$call(ctx, null, new NQPInt(1), str_box, str_box);
  }

  if (config.content.get('done')) {
    /*await*/ config.content.get('done').$$call(ctx, null, new NQPInt(result.status << 8));
  }
};

op.permit = function(handle, channel, permits) {
  // TODO Implement permit handling properly
  return handle;
};

let sigCache = null;
op.getsignals = function(currentHLL) {
  if (sigCache) {
    return sigCache;
  }

  const osSigs = os.platform() === 'win32'
    ? {
        SIGHUP: 1,
        SIGKILL: 9,
        SIGWINCH: 28,
      }
    : os.constants.signals;

  const sigWanted = [
    'SIGHUP', 'SIGINT', 'SIGQUIT', 'SIGILL', 'SIGTRAP', 'SIGABRT',
    'SIGEMT', 'SIGFPE', 'SIGKILL', 'SIGBUS', 'SIGSEGV', 'SIGSYS',
    'SIGPIPE', 'SIGALRM', 'SIGTERM', 'SIGURG', 'SIGSTOP', 'SIGTSTP',
    'SIGCONT', 'SIGCHLD', 'SIGTTIN', 'SIGTTOU', 'SIGIO', 'SIGXCPU',
    'SIGXFSZ', 'SIGVTALRM', 'SIGPROF', 'SIGWINCH', 'SIGINFO', 'SIGUSR1',
    'SIGUSR2', 'SIGTHR', 'SIGSTKFLT', 'SIGPWR', 'SIGBREAK',
  ];

  const arr = [];
  for (const k of sigWanted) {
    arr.push( core.strToObj(currentHLL, k), core.intToObj(currentHLL, k in osSigs ? osSigs[k] : 0) );
  }

  const res = hll.slurpyArray(currentHLL, arr);
  sigCache = res;
  return res;
};

op.getpid = function() {
  return process.pid;
};

op.getppid = function() {
  return process.ppid;
};

function uname(flag) {
  const result = child_process.spawnSync('uname', [flag]);
  if (result.error) {
    return "";
  } else {
    return result.stdout.toString().replace(/\n$/, '');
  }
}

op.uname = function() {
  let sysname = os.type();
  let release = os.release();
  let version = uname('-v');
  let machine = uname('-m');

  const stable = BOOT.StrArray.$$STable;
  return stable.REPR.allocateFromArray(stable, [sysname, release, version, machine]);
};
