
/**
 * Module exports.
 */

module.exports = Mode;

/**
 * Constants (defined in `stat.h`).
 */

var S_IFMT = 61440;   /* 0170000 type of file */
var S_IFIFO = 4096;   /* 0010000 named pipe (fifo) */
var S_IFCHR = 8192;   /* 0020000 character special */
var S_IFDIR = 16384;  /* 0040000 directory */
var S_IFBLK = 24576;  /* 0060000 block special */
var S_IFREG = 32768;  /* 0100000 regular */
var S_IFLNK = 40960;  /* 0120000 symbolic link */
var S_IFSOCK = 49152; /* 0140000 socket */
var S_IFWHT = 57344;  /* 0160000 whiteout */
var S_ISUID = 2048;   /* 0004000 set user id on execution */
var S_ISGID = 1024;   /* 0002000 set group id on execution */
var S_ISVTX = 512;    /* 0001000 save swapped text even after use */
var S_IRUSR = 256;    /* 0000400 read permission, owner */
var S_IWUSR = 128;    /* 0000200 write permission, owner */
var S_IXUSR = 64;     /* 0000100 execute/search permission, owner */
var S_IRGRP = 32;     /* 0000040 read permission, group */
var S_IWGRP = 16;     /* 0000020 write permission, group */
var S_IXGRP = 8;      /* 0000010 execute/search permission, group */
var S_IROTH = 4;      /* 0000004 read permission, others */
var S_IWOTH = 2;      /* 0000002 write permission, others */
var S_IXOTH = 1;      /* 0000001 execute/search permission, others */

/**
 * `Mode` class.
 *
 * @param {fs.Stat} stat a "stat" object (anything with a `mode` Number property)
 * @api public
 */

function Mode (stat) {
  if (!(this instanceof Mode)) return new Mode(stat);
  if (!stat) throw new TypeError('must pass in a "stat" object');
  if ('number' != typeof stat.mode) stat.mode = 0;
  this.stat = stat;
  this.owner = new Owner(stat);
  this.group = new Group(stat);
  this.others = new Others(stat);
}

/**
 * Returns the Number value of the `mode`.
 *
 * @return {Number}
 * @api public
 */

Mode.prototype.valueOf = function () {
  return this.stat.mode;
};

/**
 * Returns a String representation of the `mode`.
 * The output resembles something similiar to what `ls -l` would output.
 *
 * http://en.wikipedia.org/wiki/Unix_file_types
 *
 * @return {String}
 * @api public
 */

Mode.prototype.toString = function () {
  var str = [];

  // file type
  if (this.isDirectory()) {
    str.push('d');
  } else if (this.isFile()) {
    str.push('-');
  } else if (this.isBlockDevice()) {
    str.push('b');
  } else if (this.isCharacterDevice()) {
    str.push('c');
  } else if (this.isSymbolicLink()) {
    str.push('l');
  } else if (this.isFIFO()) {
    str.push('p');
  } else if (this.isSocket()) {
    str.push('s');
  } else {
    const mode = this.valueOf();
    const err = new TypeError('Unexpected "file type": mode=' + mode);
    err.stat = this.stat;
    err.mode = mode;
  }

  // owner read, write, execute
  str.push(this.owner.read ? 'r' : '-');
  str.push(this.owner.write ? 'w' : '-');
  if (this.setuid) {
    str.push(this.owner.execute ? 's' : 'S');
  } else {
    str.push(this.owner.execute ? 'x' : '-');
  }

  // group read, write, execute
  str.push(this.group.read ? 'r' : '-');
  str.push(this.group.write ? 'w' : '-');
  if (this.setgid) {
    str.push(this.group.execute ? 's' : 'S');
  } else {
    str.push(this.group.execute ? 'x' : '-');
  }

  // others read, write, execute
  str.push(this.others.read ? 'r' : '-');
  str.push(this.others.write ? 'w' : '-');
  if (this.sticky) {
    str.push(this.others.execute ? 't' : 'T');
  } else {
    str.push(this.others.execute ? 'x' : '-');
  }

  return str.join('');
};

/**
 * Returns an octal representation of the `mode`, eg. "0754".
 *
 * http://en.wikipedia.org/wiki/File_system_permissions#Numeric_notation
 *
 * @return {String}
 * @api public
 */

Mode.prototype.toOctal = function () {
  var octal = this.stat.mode & 4095 /* 07777 */;
  return ('0000' + octal.toString(8)).slice(-4);
};

Mode.prototype._checkModeProperty = function (property, set) {
  var mode = this.stat.mode;
  if (set) {
    this.stat.mode = (mode | S_IFMT) & property | mode & ~S_IFMT;
  }
  return (mode & S_IFMT) === property;
};

Mode.prototype.isDirectory = function (v) {
  return this._checkModeProperty(S_IFDIR, v);
};

Mode.prototype.isFile = function (v) {
  return this._checkModeProperty(S_IFREG, v);
};

Mode.prototype.isBlockDevice = function (v) {
  return this._checkModeProperty(S_IFBLK, v);
};

Mode.prototype.isCharacterDevice = function (v) {
  return this._checkModeProperty(S_IFCHR, v);
};

Mode.prototype.isSymbolicLink = function (v) {
  return this._checkModeProperty(S_IFLNK, v);
};

Mode.prototype.isFIFO = function (v) {
  return this._checkModeProperty(S_IFIFO, v);
};

Mode.prototype.isSocket = function (v) {
  return this._checkModeProperty(S_IFSOCK, v);
};

_define(Mode.prototype, 'setuid',
  function () {
    return Boolean(this.stat.mode & S_ISUID);
  },
  function (v) {
    if (v) {
      this.stat.mode |= S_ISUID;
    } else {
      this.stat.mode &= ~S_ISUID;
    }
  }
);

_define(Mode.prototype, 'setgid',
  function () {
    return Boolean(this.stat.mode & S_ISGID);
  },
  function (v) {
    if (v) {
      this.stat.mode |= S_ISGID;
    } else {
      this.stat.mode &= ~S_ISGID;
    }
  }
);

_define(Mode.prototype, 'sticky',
  function () {
    return Boolean(this.stat.mode & S_ISVTX);
  },
  function (v) {
    if (v) {
      this.stat.mode |= S_ISVTX;
    } else {
      this.stat.mode &= ~S_ISVTX;
    }
  }
);

function Owner (stat) {
  _define(this, 'read',
    function () {
      return Boolean(stat.mode & S_IRUSR);
    },
    function (v) {
      if (v) {
        stat.mode |= S_IRUSR;
      } else {
        stat.mode &= ~S_IRUSR;
      }
    }
  );
  _define(this, 'write',
    function () {
      return Boolean(stat.mode & S_IWUSR);
    },
    function (v) {
      if (v) {
        stat.mode |= S_IWUSR;
      } else {
        stat.mode &= ~S_IWUSR;
      }
    }
  );
  _define(this, 'execute',
    function () {
      return Boolean(stat.mode & S_IXUSR);
    },
    function (v) {
      if (v) {
        stat.mode |= S_IXUSR;
      } else {
        stat.mode &= ~S_IXUSR;
      }
    }
  );
}

function Group (stat) {
  _define(this, 'read',
    function () {
      return Boolean(stat.mode & S_IRGRP);
    },
    function (v) {
      if (v) {
        stat.mode |= S_IRGRP;
      } else {
        stat.mode &= ~S_IRGRP;
      }
    }
  );
  _define(this, 'write',
    function () {
      return Boolean(stat.mode & S_IWGRP);
    },
    function (v) {
      if (v) {
        stat.mode |= S_IWGRP;
      } else {
        stat.mode &= ~S_IWGRP;
      }
    }
  );
  _define(this, 'execute',
    function () {
      return Boolean(stat.mode & S_IXGRP);
    },
    function (v) {
      if (v) {
        stat.mode |= S_IXGRP;
      } else {
        stat.mode &= ~S_IXGRP;
      }
    }
  );
}

function Others (stat) {
  _define(this, 'read',
    function () {
      return Boolean(stat.mode & S_IROTH);
    },
    function (v) {
      if (v) {
        stat.mode |= S_IROTH;
      } else {
        stat.mode &= ~S_IROTH;
      }
    }
  );
  _define(this, 'write',
    function () {
      return Boolean(stat.mode & S_IWOTH);
    },
    function (v) {
      if (v) {
        stat.mode |= S_IWOTH;
      } else {
        stat.mode &= ~S_IWOTH;
      }
    }
  );
  _define(this, 'execute',
    function () {
      return Boolean(stat.mode & S_IXOTH);
    },
    function (v) {
      if (v) {
        stat.mode |= S_IXOTH;
      } else {
        stat.mode &= ~S_IXOTH;
      }
    }
  );
}

function _define (obj, name, get, set) {
  Object.defineProperty(obj, name, {
    enumerable: true,
    configurable: true,
    get: get,
    set: set
  });
}
