   filename: -> @_the.filename  ? (@_the.filename  = corepath.basename @basename(), @extname())
   extname : -> @_the.extension ? (@_the.extension = corepath.extname @basename())
   dirname : -> @_the.dirname   ? (@_the.dirname   = corepath.basename corepath.dirname @path)
-  extension: @extname
 
   parent: -> @_the.parent ? (@_the.parent = @to '..')
+
 
   up: (count = 1) ->
     count = Math.max 1, Math.min 20, count

fs       = require 'fs'
corepath = require 'path'

hasRoot = /// ^(
  \/           # *nix style root
  | [a-zA-Z]\: # windows drive spec
  | \\         # no drive spec, but, is absolute from current drive
) ///

class Path
  constructor: (path) ->
    @_the = {}
    if path?
      if 'string' is typeof path
        @path = path
        @parts = path.split corepath.sep # split into array
      else if Array.isArray path
        @path = path.join corepath.sep
        @parts = path.slice() # copy array
      else
        throw new Error 'Path requires either string or array as `path` to constructor'
    else
      @path = '.'
      @parts = [ '.' ]

    @isAbsolute = hasRoot.test @path
    @isRelative = not @isAbsolute

  reset: ->
    delete @_the
    delete @_stats
    @_the = {}

  stats: (done) ->
    if done?
      fs.stat @path, (error, stats) =>
        @_stats = stats
        # get rid of error if it's a 'path doesnt exist' error
        if error?.code is 'ENOENT' then error = null
        done error, stats
    else
      try
        delete @_stats
        @_stats = fs.statSync @path
      catch error
        # errno is 34 in node 0.10.*
        # errno is -2 in node 0.12.* and 4.* ... so, use 'code'
        if error.code is 'ENOENT' then return
        else throw error

  refresh: (done) ->
    @reset()
    @stats done

  toString: -> @path

  _fromStats: (done, getter) ->
    if done? then @stats (error, stats) ->
      if error?
        if error.errno is 34 then done undefined, getter()
        else done error
      else
        done undefined, getter stats

    else getter @stats()


  # options.writer: the options to pass to writer()
  # options.events: the event/listener pairs to register with the reader
  pipe: (pathOrStream, options) ->

    # get a reader for *this* path
    reader = @reader options?.reader

    if reader.error? then return reader

    if options?.events?.reader?
      reader.on event, listener for event, listener of options.events.reader

    # if target is a path then get a writer for it
    writer =
      if pathOrStream instanceof Path then pathOrStream.writer options?.writer
      else pathOrStream

    if writer.error? then return writer

    if options?.events?.writer?
      writer.on event, listener for event, listener of options.events.writer

    # finally pipe together and return result for chaining further
    return reader.pipe writer


  list: (arg) ->

    options = if typeof arg is 'function' then done:arg else arg

    unless @isDir()
      error = error: 'list() requires directory'
      if options?.done? then return options.done error
      else return error


    if options?.done?
      done = options.done
      path = this
      fs.readdir @path, (error, array) ->
        ### istanbul ignore if ###
        if error? then done error
        else done null, path._processList options, array

    else @_processList options, fs.readdirSync @path


  _processList: (options, array) ->

    acceptString = options?.acceptString
    acceptPath   = options?.acceptPath
    each         = options?.each

    # store all the accepted paths
    paths = []

    # count the rejected strings/paths
    rejectedStrings = rejectedPaths = 0

    # check array with filters, report each, count rejects
    for string in array

      # if there's no filter, or if the filter accepts
      if not acceptString? or acceptString string

        path = @to string

        # if there's no filter, or if the filter accepts
        if not acceptPath? or acceptPath path

          paths[paths.length] = path
          # call each if it exists
          each? {path}

        else rejectedPaths++

      else rejectedStrings++

    return paths:paths, rejected:{strings:rejectedStrings, paths:rejectedPaths}


  files: (arg) ->

    options = if typeof arg is 'function' then done:arg else arg

    if options?

      # clone their options
      options = Object.assign {}, options

      # wrap their filter so we can use `isFile` first
      if options.acceptPath?
        theirFilter = options.acceptPath
        options.acceptPath = (path) -> path.isFile() and theirFilter path

      # they aren't filtering, so, use ours directly
      else options.acceptPath = isFile

    # supply our files filter
    else options = acceptPath: isFile

    @list options


  dirs: (arg) ->

    options = if typeof arg is 'function' then done:arg else arg

    if options?

      # clone their options
      options = Object.assign {}, options

      # wrap their filter so we can use `isDir` first
      if options.acceptPath?
        theirFilter = options.acceptPath
        options.acceptPath = (path) -> path.isDir() and theirFilter path

      # they aren't filtering, so, use ours directly
      else options.acceptPath = isDir

    # supply our files filter
    else options = acceptPath: isDir

    @list options


  to: (path) ->

    if 'string' is typeof path
      newPath = corepath.join @path, path
      if newPath is @path then this else new Path newPath

    else if Array.isArray path then new Path @parts.concat path

    else error: 'to(path) requires either a string or array of strings'


  resolve: (path) ->
    path = corepath.resolve @path, path
    if path is @path then this else new Path path


  relativeTo: (path) ->
    path = corepath.relative @path, path
    if path.length < 1 then return this else new Path path


  normalize: () ->
    normalized = corepath.normalize @path
    if normalized is @path then this else new Path normalized


  subpath: (start, end) -> new Path (if @path is '/' then '' else @parts[start...end])


  part: (index) -> @parts[index]


# alias:
Path::extension = Path::extname


# Use these ways:
#  1a. {Path} = require 'fspath'
