
'use strict'

############################################################################################################
H                         = require './_helpers'
misfit                    = Symbol 'misfit'
platform                  = ( require 'os' ).platform()
rpr                       = ( require 'util' ).inspect
debug                     = console.log
#-----------------------------------------------------------------------------------------------------------
### Constants: ###
C_cr                      = @_C_cr            = 0x0d
C_lf                      = @_C_lf            = 0x0a
C_empty_string            = @_C_empty_string  = ''
C_empty_buffer            = @_C_empty_buffer  = Buffer.from C_empty_string
C_cr_buffer               = @_C_cr_buffer     = Buffer.from [ C_cr, ]
C_lf_buffer               = @_C_lf_buffer     = Buffer.from [ C_lf, ]

#-----------------------------------------------------------------------------------------------------------
H.types.declare 'guy_buffer_chr', ( x ) ->
  return true if ( @isa.integer x ) and ( 0x00 <= x <= 0xff )
  return true if ( @isa.buffer  x ) and ( x.length > 0 )
  return true if ( @isa.text    x ) and ( x.length > 0 )
  return false

#-----------------------------------------------------------------------------------------------------------
H.types.declare 'guy_fs_walk_buffers_cfg', tests:
  "@isa.positive_integer x.chunk_size":                               ( x ) -> @isa.positive_integer x.chunk_size

#-----------------------------------------------------------------------------------------------------------
H.types.declare 'guy_fs_walk_lines_cfg', tests:
  "@isa.object x":                                                    ( x ) -> @isa.object x
  "@isa_optional.nonempty_text x.encoding":                           ( x ) -> @isa_optional.nonempty_text x.encoding
  "@isa.positive_integer x.chunk_size":                               ( x ) -> @isa.positive_integer x.chunk_size
  "@isa.boolean x.trim":                                              ( x ) -> @isa.boolean x.trim
  "@isa.text x.prepend":                                              ( x ) -> @isa.text x.prepend
  "@isa.text x.append":                                               ( x ) -> @isa.text x.append

#-----------------------------------------------------------------------------------------------------------
H.types.declare 'guy_walk_circular_lines_cfg', tests:
  "@isa.object x":                                                    ( x ) -> @isa.object x
  "@isa.boolean x.decode":                                            ( x ) -> @isa.boolean x.decode
  "( x.loop_count is +Infinity ) or ( @isa.cardinal x.loop_count )":  ( x ) -> ( x.loop_count is +Infinity ) or ( @isa.cardinal x.loop_count )
  "( x.line_count is +Infinity ) or ( @isa.cardinal x.line_count )":  ( x ) -> ( x.line_count is +Infinity ) or ( @isa.cardinal x.line_count )

#-----------------------------------------------------------------------------------------------------------
H.types.declare 'guy_get_content_hash_cfg', tests:
  "@isa.object x":                                                    ( x ) -> @isa.object x
  "@isa.cardinal x.length":                                           ( x ) -> @isa.cardinal x.length

#-----------------------------------------------------------------------------------------------------------
defaults =
  guy_fs_walk_buffers_cfg:
    chunk_size:     16 * 1024
  guy_fs_walk_lines_cfg:
    encoding:       'utf-8'
    chunk_size:     16 * 1024
    trim:           true
    prepend:        ''
    append:         ''
  guy_walk_circular_lines_cfg:
    decode:         true
    loop_count:     1
    line_count:     +Infinity
  guy_get_content_hash_cfg:
    length:         17
    fallback:       misfit

#-----------------------------------------------------------------------------------------------------------
@walk_buffers = ( path, cfg ) ->
  for { buffer, } from @walk_buffers_with_positions path, cfg
    yield buffer
  return null

#-----------------------------------------------------------------------------------------------------------
@walk_buffers_with_positions = ( path, cfg ) ->
  H.types.validate.guy_fs_walk_buffers_cfg ( cfg = { defaults.guy_fs_walk_buffers_cfg..., cfg..., } )
  H.types.validate.nonempty_text path
  { chunk_size } = cfg
  FS            = require 'node:fs'
  fd            = FS.openSync path
  byte_idx      = 0
  loop
    buffer      = Buffer.alloc chunk_size
    byte_count  = FS.readSync fd, buffer, 0, chunk_size, byte_idx
    break if byte_count is 0
    buffer      = buffer.subarray 0, byte_count if byte_count < chunk_size
    yield { buffer, byte_idx, }
    byte_idx   += byte_count
  return null

#-----------------------------------------------------------------------------------------------------------
@walk_lines = ( path, cfg ) ->
  for { line, } from @walk_lines_with_positions path, cfg
    yield line
  return null

#-----------------------------------------------------------------------------------------------------------
@walk_lines_with_positions = ( path, cfg ) ->
  H.types.validate.guy_fs_walk_lines_cfg ( cfg = { defaults.guy_fs_walk_lines_cfg..., cfg..., } )
  H.types.validate.nonempty_text path
  { chunk_size
    encoding
    trim
    prepend
    append    } = cfg
  #.........................................................................................................
  count         = 0
  for d from @_walk_lines_with_positions path, chunk_size
    count++
    if encoding?
      d.line  = d.line.toString encoding
      d.line  = d.line.trimEnd() if trim
      d.line  = prepend + d.line  unless prepend  is ''
      d.line  = d.line  + append  unless append   is ''
      d.eol   = d.eol.toString encoding
      yield d
    else
      yield d
  return null

#-----------------------------------------------------------------------------------------------------------
@_walk_lines_with_positions = ( path, chunk_size = 16 * 1024 ) ->
  line_cache        = []
  eol_cache         = []
  lnr               = 0
  # offset_bytes      = null
  #.........................................................................................................
  flush = ( material = null, eol = null ) ->
    # return null if ( line_cache.length is 0 ) and ( eol_cache.length is 0 )
    # byte_idx          = file_byte_idx + offset_bytes
    line              = Buffer.concat if material? then [ line_cache...,  material, ] else line_cache
    eol               = Buffer.concat if eol?      then [ eol_cache...,   eol,      ] else eol_cache
    line_cache.length = 0
    eol_cache.length  = 0
    lnr++
    # yield { byte_idx, lnr, line, eol, }
    yield { lnr, line, eol, }
  #.........................................................................................................
  for { buffer, byte_idx: file_byte_idx, } from @walk_buffers_with_positions path, { chunk_size, }
    # debug '^_walk_lines_with_positions@23-4^', byte_idx, ( rpr buffer.toString() )
    # offset_bytes = 0
    for { material, eol, } from @_walk_lines__walk_advancements buffer
      # debug '^_walk_lines_with_positions@23-4^', { file_byte_idx, offset_bytes, }, ( rpr material.toString() ), ( rpr eol.toString() )
      if ( eol_cache.length > 0 ) and not ( ( ( eol_cache.at -1 ) is C_cr_buffer ) and eol is C_lf_buffer )
        yield from flush()
      switch eol
        when C_cr_buffer
          line_cache.push material if material.length  > 0
          eol_cache.push eol
        when C_lf_buffer
          yield from flush material, eol
        when C_empty_buffer
          line_cache.push material if material.length  > 0
        else throw new Error "^636456^ internal error"
      # offset_bytes += material.length + eol.length
  #.........................................................................................................
  has_extra_cr = ( eol_cache.length > 0 ) and ( ( eol_cache.at -1 ) is C_cr_buffer )
  yield from flush()
  if has_extra_cr
    lnr++
    yield { lnr, line: C_empty_buffer, eol: C_empty_buffer, }
  #.........................................................................................................
  return null

#-----------------------------------------------------------------------------------------------------------
@_walk_lines__walk_advancements = ( buffer, may_have_cr = true, may_have_lf = true ) ->
  first_idx   = 0
  last_idx    = buffer.length - 1
  loop
    break if first_idx > last_idx
    yield d = @_walk_lines__advance buffer, first_idx, may_have_cr, may_have_lf
    first_idx = d.next_idx
  return null

#-----------------------------------------------------------------------------------------------------------
### TAINT add may_have_cr, may_have_lf as optimization to forego repeated unnecessary lookups ###
@_walk_lines__advance = ( buffer, first_idx, may_have_cr = true, may_have_lf = true ) ->
  material    = C_empty_buffer
  eol         = C_empty_buffer
  next_idx_cr = -1
  next_idx_lf = -1
  next_idx_cr = buffer.indexOf C_cr, first_idx if may_have_cr
  next_idx_lf = buffer.indexOf C_lf, first_idx if may_have_lf
  next_idx    = buffer.length
  #.........................................................................................................
  if next_idx_cr is -1
    if next_idx_lf is -1
      return { material: buffer, eol, next_idx, } if first_idx is 0
    else
      next_idx    = next_idx_lf
      eol         = C_lf_buffer
  #.........................................................................................................
  else if ( next_idx_lf is -1 ) or ( next_idx_cr < next_idx_lf )
    next_idx    = next_idx_cr
    eol         = C_cr_buffer
  #.........................................................................................................
  else
    next_idx    = next_idx_lf
    eol         = C_lf_buffer
  #.........................................................................................................
  material = buffer.subarray first_idx, next_idx
  return { material, eol, next_idx: next_idx + 1, }


#===========================================================================================================
#
#-----------------------------------------------------------------------------------------------------------
@walk_circular_lines = ( path, cfg ) ->
  H.types.validate.guy_walk_circular_lines_cfg ( cfg = { defaults.guy_walk_circular_lines_cfg..., cfg..., } )
  return if ( cfg.line_count is 0 ) or ( cfg.loop_count is 0 )
  line_count = 0
  loop_count = 0
  loop
    for line from @walk_lines path, { decode: cfg.decode, }
      yield line
      line_count++; return null if line_count >= cfg.line_count
    loop_count++; return null if loop_count >= cfg.loop_count
  return null

#-----------------------------------------------------------------------------------------------------------
@get_file_size = ( path, fallback = misfit ) ->
  H.types.validate.nonempty_text path
  try return ( ( require 'fs' ).statSync path ).size catch error
    throw error if ( fallback is misfit )
    throw error if ( error.code isnt 'ENOENT' )
  return fallback

#-----------------------------------------------------------------------------------------------------------
@get_content_hash = ( path, cfg ) ->
  H.types.validate.nonempty_text path
  H.types.validate.guy_get_content_hash_cfg ( cfg = { defaults.guy_get_content_hash_cfg..., cfg..., } )
  CP        = require 'child_process'
  command   = if platform is 'linux' then 'sha1sum' else 'shasum'
  result    = CP.spawnSync command, [ '-b', path, ]
  if result.status isnt 0
    return cfg.fallback unless cfg.fallback is misfit
    if result.stderr?
      throw new Error "^guy.fs.get_content_hash@1^ " + result.stderr.toString 'utf-8'
    else
      throw new Error "^guy.fs.get_content_hash@1^ " + ( require 'util' ).inspect result.error
  R = result
  R = R.stdout.toString 'utf-8'
  R = R.replace /\s.*$/, ''
  R = R[ ... cfg.length ]
  unless R.length is cfg.length
    throw new Error "^guy.fs.get_content_hash@1^ unable to generate hash of length #{cfg.length} using #{command}"
  return R

#-----------------------------------------------------------------------------------------------------------
@rename_sync = ( from_path, to_path ) ->
  ### Same as `FS.renameSync()`, but falls back to `FS.copyFileSync()`, `FS.unlinkSync()` in case device
  boundaries are crossed. Thx to https://github.com/sindresorhus/move-file/blob/main/index.js ###
  FS = require 'node:fs'
  try FS.renameSync from_path, to_path catch error
    throw error unless error.code is 'EXDEV'
    FS.copyFileSync from_path, to_path
    FS.unlinkSync from_path
  return null

#-----------------------------------------------------------------------------------------------------------
_get_descriptor_types_and_methods = [
  { type: 'file',       method_name: 'isFile',            }
  { type: 'folder',     method_name: 'isDirectory',       }
  { type: 'block',      method_name: 'isBlockDevice',     }
  { type: 'character',  method_name: 'isCharacterDevice', }
  { type: 'fifo',       method_name: 'isFIFO',            }
  { type: 'socket',     method_name: 'isSocket',          } ]

#-----------------------------------------------------------------------------------------------------------
@_get_descriptor = ( path ) ->
  FS      = require 'node:fs'
  type    = null
  link    = false
  is_loop = false
  #.........................................................................................................
  [ lstat, link, ] = do =>
    try lstat = FS.lstatSync path catch error
      return [ null, false,  ] if error.code is 'ENOENT'
      throw error
    return [ lstat, lstat.isSymbolicLink(), ]
  #.........................................................................................................
  return { type, link, is_loop, } unless lstat?
  #.........................................................................................................
  if link then [ stat, is_loop, ] = do =>
    try stat = FS.statSync path catch error
      return [ null,  true,   ] if error.code is 'ELOOP'
      return [ null,  false,  ] if error.code is 'ENOENT'
      throw error
    return [ stat, false, ]
  is_loop  ?= false
  stat      = lstat if is_loop or ( not link )
  return { type: 'link', link, is_loop, } if ( not stat? ) and ( not is_loop )
  type = do =>
    for { type, method_name, } in _get_descriptor_types_and_methods
      return type if stat[ method_name ]()
    return null
  #.........................................................................................................
  type ?= 'link'
  return { type, link, is_loop, }

#-----------------------------------------------------------------------------------------------------------
@get_descriptor = ( P... ) -> @_get_descriptor P...

# #-----------------------------------------------------------------------------------------------------------
# get_long_file_descriptor = ( P... ) ->
#   throw new Error "not implemented"
#   { dsc, stats } = _get_descriptor P...
# dev: 2114n,
# ino: 48064969n,
# mode: 33188n,
# nlink: 1n,
# uid: 85n,
# gid: 100n,
# rdev: 0n,
# size: 527n,
# blksize: 4096n,
# blocks: 8n,
# atimeMs: 1318289051000n,
# mtimeMs: 1318289051000n,
# ctimeMs: 1318289051000n,
# birthtimeMs: 1318289051000n,
# atimeNs: 1318289051000000000n,
# mtimeNs: 1318289051000000000n,
# ctimeNs: 1318289051000000000n,
# birthtimeNs: 1318289051000000000n,
# atime: Mon, 10 Oct 2011 23:24:11 GMT,
# mtime: Mon, 10 Oct 2011 23:24:11 GMT,
# ctime: Mon, 10 Oct 2011 23:24:11 GMT,
# birthtime: Mon, 10 Oct 2011 23:24:11 GMT



