;=======================================
; cp_main: Copy a file (supports copying
; across devices and directories).
;=======================================

program cp_main

include library
include prosys
include c64
include 1:diskutils.e

;name offset in directory entry
con dtype=0 ;file type offset in dirent
con dname=3 ;name offset in dirent
con dirlen=32
con SRC=1
con DEST=2
con TRY_AGAIN =$ff01
con NO_REPLACE=$ff02
con CANCEL    =$ff03

word err
word pattern ;directory matching pattern
word srcname
word destname
word bufptr
word files
word firstfile ;array of byte[17]
word nextfile  ;filetype (S/P) + name
word copied

byte cmdbuf[64]
byte dirname[254]
byte srcfile[254]
byte destfile[254]
byte buffer[254]
byte column
byte row
byte sdev
byte ddev
byte wildcard ;boolean (wildcard in src)
byte colon    ;offset of colon in src
byte lfndir
byte lfnsrc
byte lfndest
byte replace_one ;boolean
byte replace_all ;boolean

;--------------------------------------
; Print usage message.
;--------------------------------------
proc usage

begin
output"#cUsage: #s -s<src device> <src name>",carg[0]
output"#c       -d<dest device> <dest name>"
output"#c(CMD syntax, SEQ and PRG files only)#c"
end

;--------------------------------------
; Filename sanity check.
; pass: pointer to filename
; pass: constant: src or dest
; return: True if sane, false otherwise.
;--------------------------------------
func byte sane
arg word name
arg word which
byte slash
byte colon
byte sanity
word i

begin

slash=false
colon=false
for i=0 to lenstr(name)-1
  if (name+i)@<='/'
    slash=true
  if (name+i)@<=':'
    if colon ;more than one
      output"#cMore than one colon found!#c"
      return false
    colon=true
if slash and not colon
  output"#cName with slash must also have colon!#c"
  return false

if which=SRC
  if (name+lenstr(name)-1)@<=':'
    output"#cSource can't end with ':'!#c"
    return false
  if cmpstr(name,"=",".")
    output"#cCan't use '.' for source!#c"
    return false
else if which=DEST
  for i=0 to lenstr(name)-1
    if (name+i)@<='*' or (name+i)@<='?'
      output"#cNo wildcards in destination!#c"
      return false
    i=name+lenstr(name)-1
    if i@<=':' and (i-1)@<<>'/'
      output"#cDirectory must end with #s/:#s!#c",$22,$22
      return false

sanity=false
i=0
while(name+i)@<
  if (name+i)@<<>'/' and (name+i)@<<>':'
    sanity=true
    break
  i=i+1
if not sanity
  output"#cInvalid filename!#c"
  return false

return true

end

;--------------------------------------
; Parse and set device number.
; pass: address of string to parse
; return: device number if valid, else 0
;--------------------------------------
func byte parsedev
arg word devstr
word w
byte b

begin

if cmpstr(devstr,"=","0:")
  return c64ddv0
else if cmpstr(devstr,"=","1:")
  return c64ddv1
else
  b=strval(devstr,#w) ;w must be word
  if (devstr+b)@< ;not numeric?
    return 0
  else
    if w<8 or w>30
      return 0
    else
      return w

end

;--------------------------------------
; Parse command-line arguments.
; return: true if arguments are valid,
;         false otherwise
;--------------------------------------
func byte parse
word i
word j
word k
byte b

begin

if ncarg<>4
  return false

sdev=0
ddev=0
srcname=0
destname=0
wildcard=false
pattern=0
dirname[0]=0

for i=1 to ncarg ;0 is program name
  ;args are folded to upper-case!
  if cmpstr(carg[i],"=","-S",false,2)
    sdev=parsedev(carg[i]+2)
    if sdev=0
      return false
  else if cmpstr(carg[i],"=","-D",false,2)
    ddev=parsedev(carg[i]+2)
    if ddev=0
      return false
  else ;filename
    if srcname
      ;src name parsed, this is dest
      if not sane(carg[i],DEST)
        return false
      destname=carg[i]
    else
      ;this is src, check path/pattern
      if not sane(carg[i],SRC)
        return false
      colon=0
      for j=0 to lenstr(carg[i])-1
        if (carg[i]+j)@<=':'
          colon=j
      if colon>0
        if colon=lenstr(carg[i])-1
          output"#cSource can't end with ':'!#c"
          return false
        else
          j=colon+1
          dirname[0]='$'
          for k=0 to colon-1
            if (carg[i]+k)@<='*' or (carg[i]+j)@<='?'
              output"#cNo wildcards in source directory!#c"
              return false
            dirname[k+1]=(carg[i]+k)@<
          ;all files (manual wildcards)
          dirname[colon+1]=':'
          dirname[colon+2]='*'
          dirname[colon+3]=0
          pattern=carg[i]+colon+1
      else ;needed to check file type
        j=0
        dirname[0]='$'
        dirname[1]=':'
        dirname[2]='*'
        dirname[3]=0
        pattern=carg[i]

      while j<lenstr(carg[i])
        if (carg[i]+j)@<='*' or (carg[i]+j)@<='?'
          wildcard=true
          break
        j=j+1
      srcname=carg[i]
      
if sdev=0 or ddev=0
  return false
if drives[sdev-8]=0
  output"#cDrive #w not present!",sdev
  return false
if drives[ddev-8]=0
  output"#cDrive #w not present!",ddev
  return false

if srcname=0 or destname=0
  return false

if wildcard and cmpstr(destname,"<>",".")
  ;if src is wildcard, dest must be dir
  i=destname+lenstr(destname)-1
  if (i-1)@<<>'/' or i@<<>':'
    output"#cCan't copy wildcard to filename!"
    output"#c(directory must end with #s/:#s)#c",$22,$22
    return false

return true

end

;--------------------------------------
; Open directory as a file.
; pass: device number 
; return: error code 
;--------------------------------------
func word opendir
arg byte dev
word err

begin

lfndir=getlfn
if lfndir=255
  output"#cNo free file handles!"
  return E_DOS+70
err=sendcmd(dev,"I") ;drvquery validated
jsr csetlfs,lfndir,dev,lfndir
jsr csetnam,lenstr(dirname),dirname:<,dirname:>
jsr copen
if regf and regfc
  err=E_ACCUM+rega
else
  err=readcmd(dev,cmdbuf)
  if err=0
    err=(cmdbuf[0] and $0f)*10
    err=err+(cmdbuf[1] and $0f)
    if cmdbuf[lenstr(cmdbuf)-1]=$0d
      cmdbuf[lenstr(cmdbuf)-1]=0
    if err>0
      output"#c#s",cmdbuf
      err=err+E_DOS

return err

end

;--------------------------------------
; Read one sector of directory file
; (call opendir first).
; return: error code (status byte)
;--------------------------------------
func word readdir
word err
byte first
byte eof
byte temp

begin
bufptr=buffer
m[cstatus]=0
err=0

jsr ctalk,sdev
if m[cstatus] > 127
  return E_STATUS+m[cstatus]
jsr ctksa,lfndir or $60
if m[cstatus] > 127
  jsr cuntlk
  return E_STATUS+m[cstatus]

first=true
eof=false
repeat
  jsr cacptr
  if m[cstatus]<>0
    if m[cstatus] and $40 ;eof?
      err=E_STATUS+m[cstatus]
      eof=true
      if first
        break
    else
      temp=m[cstatus]
      jsr cuntlk
      return E_STATUS+temp
  else
    first=false
  m[bufptr]=m[$a4] ;cached by ACPTR
  bufptr=bufptr+1
  if bufptr=buffer+254
    eof=true ;end of this sector
until eof
jsr cuntlk

return err

end

;---------------------------------------
; Case-insensitive match of two
; non-wildcard characters.
; pass: name character
; pass: pattern character
;---------------------------------------
func byte cimatch
arg byte nb 
arg byte pb

begin

if nb>='a' and nb<='z'
  nb=nb and $df
if pb>='a' and pb<='z'
  pb=pb and $df
return nb=pb

end

;---------------------------------------
; Match filename against pattern.
; pass: filename, pattern
; return: true on match, else false 
;---------------------------------------
func byte match
arg word name
arg word pattern
byte matched  ;boolean
byte matching ;boolean

begin

if pattern=0
  return true

matched=true
while matched
  if not name@<
    if not pattern@<
      break
    else if pattern@<='*'
      if (pattern+1)@<
        matched=false
      break
    else
      matched=false
      break
  else
    if not pattern@<
      matched=false
      break
    else if pattern@<='*'
      pattern=pattern+1
      if not pattern@<
        break
      else
        matching=true
        while matching
          if cimatch(name@<,pattern@<)
            matching=false
            break
          else if not name@<
            matching=false
            matched=false
          name=name+1
    else if not cimatch(name@<,pattern@<) and pattern@<<>'?'
      matched=false
      break
  name=name+1
  pattern=pattern+1

return matched

end

;--------------------------------------
; Find matching files to copy. Must be
; called for single files as well to
; find the dir entry and check the file
; type (ignore DEL,USR,REL).
; return: error code
;--------------------------------------
func word findfiles
word i
word j
byte filetype

begin

err=opendir(sdev) ;opens dirname, sets lfndir
if err<>0
  output"#cError opening source directory!",err
  jsr cclose,lfndir
  return err
err=readdir ;ignore header
repeat
  err=readdir
  if err<>0 and err<>E_STATUS+$40
    break
  bufptr=buffer
  for i=1 to 8
    ;SEQ and PRG files only!
    filetype=(bufptr+dtype)@< and $0f
    if filetype=$01
      filetype='S'
    else if filetype=$02
      filetype='P'
    else
      filetype=0
    if filetype='S' or filetype='P'
      j=15 ;null-terminate filename
      while (bufptr+dname+j)@<=$a0
        m[bufptr+dname+j]=0
        j=j-1
      m[bufptr+dname+16]=0 ;kills rel ss
      ;match returns true if no pattern
      if match(bufptr+dname,pattern)
        m[nextfile]=filetype
        for j=0 to 15
          m[nextfile+j+1]=(bufptr+dname+j)@<
        nextfile=nextfile+17
        if nextfile>=memlim
          abort "*** OUT OF MEMORY ***"

        files=files+1
    bufptr=bufptr+dirlen
until err<>0 ;until error or eof
jsr cclose,lfndir

if err<>0 and err<>E_STATUS+$40
  output"#cError $#04h reading source directory!",err

return err

end

;--------------------------------------
; Open a file.
;--------------------------------------
func word openfile
arg byte lfn
arg byte dev
arg word name
word err

begin

jsr csetlfs,lfn,dev,lfn
jsr csetnam,lenstr(name),name:<,name:>
jsr copen
if regf and regfc
  return E_ACCUM+rega
err=readcmd(dev,cmdbuf)
err=(cmdbuf[0] and $0f)*10
err=err+(cmdbuf[1] and $0f)
if err=63 ;file exists
  jsr cclose,lfn
  output"#cReplace file? (Yes/No/All/Cancel) "
  err=0
  while err=0
    choose toupper(getc)
    'Y'
      replace_one=true
      err=TRY_AGAIN
    'N'
      err=NO_REPLACE
    'A'
      replace_all=true
      err=TRY_AGAIN
    'C'
      err=CANCEL
    else
      put $14 ;delete
else if err>0
  jsr cclose,lfn
  output"#c#s",cmdbuf
  err=E_DOS+err

return err

end

;--------------------------------------
; Copy a file.
; pass: source filename
; pass: destination filename
; return: error code
;--------------------------------------
func word copyfile
arg word src
arg word dest
word err
word w
byte srclfn
byte destlfn
byte ptr
byte eof

begin

;print basename
output"#c#s (#w/#w)",nextfile+1,copied+1,files

if replace_one or replace_all
  movstr dest,dest+3
  m[dest]='@'
  m[dest+1]='0'
  m[dest+2]=':'

srclfn=getlfn
if srclfn=255
  output"#cNo free file handles!"
  return E_DOS+70
err=openfile(srclfn,sdev,src)
if err<>0
  return err

destlfn=getlfn
if destlfn=255
  output"#cNo free file handles!"
  jsr cclose,srclfn
  return E_DOS+70
err=openfile(destlfn,ddev,dest)
if err<>0
  jsr cclose,srclfn
  return err

eof=255 ;can never become 255
while eof=255
  put '.' ;progress meter
  m[cstatus]=0
  jsr ctalk,sdev
  if m[cstatus]<>0
    err=E_STATUS+m[cstatus]
    break
  jsr ctksa,srclfn or $60
  if m[cstatus]<>0
    err=E_STATUS+m[cstatus]
    jsr cuntlk
    break
  ptr=0
  while ptr<254
    jsr cacptr
    buffer[ptr]=rega
    if m[cstatus] and $40
      eof=ptr
      break
    ptr=ptr+1
    if m[cstatus]<>0
      err=E_STATUS+m[cstatus]
      break
  jsr cuntlk
  if err>0
    break

  m[cstatus]=0
  jsr clisten,ddev
  if m[cstatus]<>0
    err=E_STATUS+m[cstatus]
    break
  jsr csecond,destlfn or $60
  if m[cstatus]<>0
    err=E_STATUS+m[cstatus]
    jsr cunlsn
    break
  ptr=0
  while ptr<254
    jsr cciout,buffer[ptr]
    if m[cstatus]<>0
      err=E_STATUS+m[cstatus]
      break
    if ptr=eof
      break
    ptr=ptr+1
  jsr cunlsn
  if err>0
    break

jsr cclose,srclfn
jsr cclose,destlfn
return err

end

;--------------------------------------
; Copy matching files.
;--------------------------------------
func word copyfiles
word err
word i
word j
word k
byte destptr

begin

err=0
nextfile=firstfile
replace_one=false
replace_all=false

if wildcard ;destname will end w/colon
  ;colon is +1 in dirname, starts w/$
  for i=0 to colon
    srcfile[i]=dirname[i+1]
  if cmpstr(destname,"<>",".")
    i=0
    while (destname+i)@<
      destfile[i]=(destname+i)@<
      i=i+1
    destptr=i
  i=0 ;file counter
  repeat
    if colon>0 ;colon in srcname?
      k=colon+1
    else
      k=0
    ;may use all 16 chars w/o null-term.
    j=0
    repeat
      srcfile[k]=(nextfile+1+j)@<
      k=k+1
      if cmpstr(destname,"=",".")
        destfile[j]=(nextfile+1+j)@<
      else
        destfile[destptr+j]=(nextfile+1+j)@<
      j=j+1
    until j=16 or not (nextfile+1+j)@<
    srcfile[k]=','
    srcfile[k+1]=nextfile@< ;filetype
    srcfile[k+2]=','
    srcfile[k+3]='R'
    srcfile[k+4]=0

    if cmpstr(destname,"<>",".")
      j=j+destptr
    destfile[j]=','
    destfile[j+1]=nextfile@< ;filetype
    destfile[j+2]=','
    destfile[j+3]='W'
    destfile[j+4]=0

    err=0
    repeat
      err=copyfile(srcfile,destfile)
    until err<>TRY_AGAIN
    replace_one=false

    if err=0
      copied=copied+1
    if err=NO_REPLACE
      err=0
    if err<> 0 ;including CANCEL
      break

    nextfile=nextfile+17
    i=i+1
  until i=files
else ;single file
  i=0
  while (srcname+i)@<
    srcfile[i]=(srcname+i)@<
    i=i+1
  srcfile[i]=','
  srcfile[i+1]=nextfile@< ;filetype
  srcfile[i+2]=','
  srcfile[i+3]='R'
  srcfile[i+4]=0

  if cmpstr(destname,"=",".")
    i=0
    if colon>0 ;colon in source name?
      j=colon+1
    else
      j=0
    while (srcname+j)@<
      destfile[i]=(srcname+j)@<
      j=j+1
      i=i+1
    destfile[i]=','
    destfile[i+1]=nextfile@< ;filetype
    destfile[i+2]=','
    destfile[i+3]='W'
    destfile[i+4]=0
  else
    i=0
    while (destname+i)@<
      destfile[i]=(destname+i)@<
      i=i+1
    ;destination a directory?
    if (destname+lenstr(destname)-1)@<=':'
      if colon>0 ;colon in source name?
        j=colon+1
      else
        j=0
      while (srcname+j)@<
        destfile[i]=(srcname+j)@<
        j=j+1
        i=i+1
    destfile[i]=','
    destfile[i+1]=nextfile@< ;filetype
    destfile[i+2]=','
    destfile[i+3]='W'
    destfile[i+4]=0
  repeat
    err=copyfile(srcfile,destfile)
  until err<>TRY_AGAIN
  if err=0
    copied=copied+1
  if err=NO_REPLACE
    err=0

if err=CANCEL
  err=0
return err

end

;======================================
; program mainline
;======================================

begin

drvquery

if not parse
  usage
  exit

edres=0
firstfile=himem
nextfile=himem
files=0
copied=0

column=curcol
row=curline
output"#cSearching for matching file(s)..."
err=findfiles
curset column,row ;erase message
output"#c                                 "
curset column,row

if err<>0 and err <> E_STATUS+$40
  output"#cError finding files!#c"
  exit

if files>0
  output"#c#w files found to copy.",files
  err=copyfiles
  if err<>0 and err<>E_STATUS+$40
    output"#c#cError $#04h copying files!",err
  else
    output"#c#cFiles copied: #w#c",copied
else
  output"#c#cNo files found to copy!#c"

end
