///|
/// State of a bounded IEC 104 file transfer session.
pub enum FileTransferState {
  FileIdle
  FileSelecting
  FileTransferring
  FileWaitingForAck
  FileCompleted
  FileFailed
  FileCancelled
} derive(Eq, Debug)

///|
/// Direction of a file service request.
pub enum FileTransferDirection {
  UploadFile
  DownloadFile
} derive(Eq, Debug)

///|
/// A file identity used by the file service layer.
pub struct FileIdentity {
  station : CommonAddress
  name : String
  file_id : Int
} derive(Eq, Debug)

///|
pub fn FileIdentity::new(
  station : CommonAddress,
  name : String,
  file_id : Int,
) -> Result[FileIdentity, String] {
  if name.length() == 0 || name.length() > 255 {
    Err("file name must contain between 1 and 255 characters")
  } else if file_id < 0 || file_id > 0xffff {
    Err("file id must fit sixteen bits")
  } else {
    Ok({ station, name, file_id })
  }
}

///|
pub fn FileIdentity::station(self : FileIdentity) -> CommonAddress {
  self.station
}

///|
pub fn FileIdentity::name(self : FileIdentity) -> String {
  self.name
}

///|
pub fn FileIdentity::file_id(self : FileIdentity) -> Int {
  self.file_id
}

///|
/// Metadata and immutable content for one locally retained file.
pub struct StoredFile {
  identity : FileIdentity
  data : Bytes
  created_at : Int
  updated_at : Int
  revision : Int
  checksum : UInt
} derive(Eq, Debug)

///|
pub fn StoredFile::new(
  identity : FileIdentity,
  data : Bytes,
  timestamp : Int,
) -> StoredFile {
  {
    identity,
    data,
    created_at: timestamp,
    updated_at: timestamp,
    revision: 1,
    checksum: crc32_ieee(data),
  }
}

///|
pub fn StoredFile::identity(self : StoredFile) -> FileIdentity {
  self.identity
}

///|
pub fn StoredFile::data(self : StoredFile) -> Bytes {
  self.data
}

///|
pub fn StoredFile::size(self : StoredFile) -> Int {
  self.data.length()
}

///|
pub fn StoredFile::created_at(self : StoredFile) -> Int {
  self.created_at
}

///|
pub fn StoredFile::updated_at(self : StoredFile) -> Int {
  self.updated_at
}

///|
pub fn StoredFile::revision(self : StoredFile) -> Int {
  self.revision
}

///|
pub fn StoredFile::checksum(self : StoredFile) -> UInt {
  self.checksum
}

///|
/// Segment of a file containing an offset and an end marker.
pub struct FileSegment {
  file_id : Int
  section : Int
  offset : Int
  data : Bytes
  last : Bool
  checksum : UInt
} derive(Eq, Debug)

///|
pub fn FileSegment::new(
  file_id : Int,
  section : Int,
  offset : Int,
  data : Bytes,
  last : Bool,
) -> Result[FileSegment, String] {
  if file_id < 0 || section < 0 || offset < 0 {
    Err("file segment coordinates cannot be negative")
  } else if data.length() == 0 && !last {
    Err("empty non-terminal file segment is invalid")
  } else {
    Ok({ file_id, section, offset, data, last, checksum: crc32_ieee(data) })
  }
}

///|
pub fn FileSegment::file_id(self : FileSegment) -> Int {
  self.file_id
}

///|
pub fn FileSegment::section(self : FileSegment) -> Int {
  self.section
}

///|
pub fn FileSegment::offset(self : FileSegment) -> Int {
  self.offset
}

///|
pub fn FileSegment::data(self : FileSegment) -> Bytes {
  self.data
}

///|
pub fn FileSegment::last(self : FileSegment) -> Bool {
  self.last
}

///|
pub fn FileSegment::checksum(self : FileSegment) -> UInt {
  self.checksum
}

///|
pub fn FileSegment::valid_checksum(self : FileSegment) -> Bool {
  crc32_ieee(self.data) == self.checksum
}

///|
/// A file inventory entry returned to operational clients.
pub struct FileInventoryEntry {
  identity : FileIdentity
  size : Int
  revision : Int
  checksum : UInt
  updated_at : Int
} derive(Eq, Debug)

///|
pub fn FileInventoryEntry::identity(self : FileInventoryEntry) -> FileIdentity {
  self.identity
}

///|
pub fn FileInventoryEntry::size(self : FileInventoryEntry) -> Int {
  self.size
}

///|
pub fn FileInventoryEntry::revision(self : FileInventoryEntry) -> Int {
  self.revision
}

///|
pub fn FileInventoryEntry::checksum(self : FileInventoryEntry) -> UInt {
  self.checksum
}

///|
pub fn FileInventoryEntry::updated_at(self : FileInventoryEntry) -> Int {
  self.updated_at
}

///|
/// A bounded in-memory store used by outstations and gateway adapters.
pub struct FileStore {
  files : Map[String, StoredFile]
  capacity : Int
  maximum_size : Int
} derive(Debug)

///|
pub fn FileStore::new(
  capacity? : Int = 128,
  maximum_size? : Int = 1048576,
) -> Result[FileStore, String] {
  if capacity < 1 || maximum_size < 1 {
    Err("file store limits must be positive")
  } else {
    Ok({ files: {}, capacity, maximum_size })
  }
}

///|
pub fn FileStore::len(self : FileStore) -> Int {
  self.files.length()
}

///|
pub fn FileStore::capacity(self : FileStore) -> Int {
  self.capacity
}

///|
pub fn FileStore::maximum_size(self : FileStore) -> Int {
  self.maximum_size
}

///|
pub fn FileStore::get(self : FileStore, name : String) -> StoredFile? {
  self.files.get(name)
}

///|
pub fn FileStore::inventory(self : FileStore) -> Array[FileInventoryEntry] {
  let result : Array[FileInventoryEntry] = []
  for _, file in self.files {
    result.push({
      identity: file.identity(),
      size: file.size(),
      revision: file.revision(),
      checksum: file.checksum(),
      updated_at: file.updated_at(),
    })
  }
  result.sort_by((left, right) => {
    if left.identity().station().number() != right.identity().station().number() {
      left.identity().station().number() - right.identity().station().number()
    } else {
      left.identity().name().compare(right.identity().name())
    }
  })
  result
}

///|
pub fn FileStore::put(
  self : FileStore,
  identity : FileIdentity,
  data : Bytes,
  timestamp : Int,
) -> Result[StoredFile, String] {
  if data.length() > self.maximum_size {
    Err("file exceeds configured maximum size")
  } else if self.files.length() >= self.capacity &&
    self.files.get(identity.name()) is None {
    Err("file store capacity is full")
  } else {
    let file = match self.files.get(identity.name()) {
      Some(existing) =>
        {
          identity,
          data,
          created_at: existing.created_at(),
          updated_at: timestamp,
          revision: existing.revision() + 1,
          checksum: crc32_ieee(data),
        }
      None => StoredFile::new(identity, data, timestamp)
    }
    self.files[file.identity().name()] = file
    Ok(file)
  }
}

///|
pub fn FileStore::remove(
  self : FileStore,
  name : String,
) -> Result[StoredFile, String] {
  match self.files.get(name) {
    None => Err("file does not exist")
    Some(file) => {
      self.files.remove(name)
      Ok(file)
    }
  }
}

///|
/// File transfer session with ordered segment assembly.
pub struct FileTransferSession {
  ticket : Int
  identity : FileIdentity
  direction : FileTransferDirection
  expected_size : Int
  segment_size : Int
  expected_checksum : UInt?
  buffer : Array[Byte]
  mut next_offset : Int
  mut next_section : Int
  mut state : FileTransferState
  mut attempts : Int
  mut last_activity : Int
  mut error : String
} derive(Debug)

///|
pub fn FileTransferSession::new(
  ticket : Int,
  identity : FileIdentity,
  direction : FileTransferDirection,
  expected_size : Int,
  segment_size : Int,
  expected_checksum : UInt?,
  timestamp : Int,
) -> Result[FileTransferSession, String] {
  if ticket < 1 || expected_size < 0 {
    Err("file transfer ticket and size must be non-negative")
  } else if segment_size < 1 || segment_size > 240 {
    Err("file segment size must be between 1 and 240 bytes")
  } else {
    Ok({
      ticket,
      identity,
      direction,
      expected_size,
      segment_size,
      expected_checksum,
      buffer: [],
      next_offset: 0,
      next_section: 0,
      state: FileSelecting,
      attempts: 0,
      last_activity: timestamp,
      error: "",
    })
  }
}

///|
pub fn FileTransferSession::ticket(self : FileTransferSession) -> Int {
  self.ticket
}

///|
pub fn FileTransferSession::identity(
  self : FileTransferSession,
) -> FileIdentity {
  self.identity
}

///|
pub fn FileTransferSession::direction(
  self : FileTransferSession,
) -> FileTransferDirection {
  self.direction
}

///|
pub fn FileTransferSession::expected_size(self : FileTransferSession) -> Int {
  self.expected_size
}

///|
pub fn FileTransferSession::segment_size(self : FileTransferSession) -> Int {
  self.segment_size
}

///|
pub fn FileTransferSession::expected_checksum(
  self : FileTransferSession,
) -> UInt? {
  self.expected_checksum
}

///|
pub fn FileTransferSession::received_size(self : FileTransferSession) -> Int {
  self.buffer.length()
}

///|
pub fn FileTransferSession::next_offset(self : FileTransferSession) -> Int {
  self.next_offset
}

///|
pub fn FileTransferSession::next_section(self : FileTransferSession) -> Int {
  self.next_section
}

///|
pub fn FileTransferSession::state(
  self : FileTransferSession,
) -> FileTransferState {
  self.state
}

///|
pub fn FileTransferSession::attempts(self : FileTransferSession) -> Int {
  self.attempts
}

///|
pub fn FileTransferSession::last_activity(self : FileTransferSession) -> Int {
  self.last_activity
}

///|
pub fn FileTransferSession::error(self : FileTransferSession) -> String {
  self.error
}

///|
pub fn FileTransferSession::data(self : FileTransferSession) -> Bytes {
  Bytes::from_array(self.buffer.copy())
}

///|
pub fn FileTransferSession::activate(
  self : FileTransferSession,
  timestamp : Int,
) -> Result[Unit, String] {
  if self.state != FileSelecting {
    Err("file session is not selectable")
  } else {
    self.state = FileTransferring
    self.last_activity = timestamp
    Ok(())
  }
}

///|
pub fn FileTransferSession::append(
  self : FileTransferSession,
  segment : FileSegment,
  timestamp : Int,
) -> Result[Unit, String] {
  if self.state != FileTransferring && self.state != FileWaitingForAck {
    return Err("file session is not receiving segments")
  }
  if segment.file_id() != self.identity.file_id() {
    return Err("file segment belongs to another file")
  }
  if segment.section() != self.next_section ||
    segment.offset() != self.next_offset {
    return Err("file segment is out of order")
  }
  if !segment.valid_checksum() {
    return Err("file segment checksum mismatch")
  }
  if self.received_size() + segment.data().length() > self.expected_size {
    return Err("file transfer exceeds expected size")
  }
  for byte in segment.data().to_array() {
    self.buffer.push(byte)
  }
  self.next_offset += segment.data().length()
  self.next_section += 1
  self.last_activity = timestamp
  self.attempts += 1
  if segment.last() {
    if self.received_size() != self.expected_size {
      self.state = FileFailed
      self.error = "terminal segment has unexpected size"
      Err(self.error)
    } else {
      self.state = FileCompleted
      Ok(())
    }
  } else {
    self.state = FileWaitingForAck
    Ok(())
  }
}

///|
pub fn FileTransferSession::ack(
  self : FileTransferSession,
  timestamp : Int,
) -> Result[Unit, String] {
  if self.state != FileWaitingForAck {
    Err("file session is not waiting for acknowledgement")
  } else {
    self.state = FileTransferring
    self.last_activity = timestamp
    Ok(())
  }
}

///|
pub fn FileTransferSession::fail(
  self : FileTransferSession,
  timestamp : Int,
  message : String,
) -> Unit {
  self.state = FileFailed
  self.last_activity = timestamp
  self.error = message
}

///|
pub fn FileTransferSession::cancel(
  self : FileTransferSession,
  timestamp : Int,
) -> Result[Unit, String] {
  if self.state == FileCompleted || self.state == FileFailed {
    Err("terminal file session cannot be cancelled")
  } else {
    self.state = FileCancelled
    self.last_activity = timestamp
    Ok(())
  }
}

///|
/// Generate ordered download segments from an immutable file.
pub fn split_file_segments(
  file : StoredFile,
  segment_size : Int,
) -> Result[Array[FileSegment], String] {
  if segment_size < 1 || segment_size > 240 {
    return Err("file segment size is outside 1..240")
  }
  let bytes = file.data().to_array()
  let result : Array[FileSegment] = []
  if bytes.length() == 0 {
    match FileSegment::new(file.identity().file_id(), 0, 0, b"", true) {
      Ok(segment) => result.push(segment)
      Err(_) => ()
    }
  } else {
    let mut offset = 0
    let mut section = 0
    while offset < bytes.length() {
      let end = if offset + segment_size > bytes.length() {
        bytes.length()
      } else {
        offset + segment_size
      }
      let last = end == bytes.length()
      match
        FileSegment::new(
          file.identity().file_id(),
          section,
          offset,
          Bytes::from_array(bytes[offset:end]),
          last,
        ) {
        Ok(segment) => result.push(segment)
        Err(error) => return Err(error)
      }
      offset = end
      section += 1
    }
  }
  Ok(result)
}

///|
/// Counters for file service throughput and failures.
pub struct FileTransferMetrics {
  mut sessions : Int
  mut completed : Int
  mut failed : Int
  mut cancelled : Int
  mut segments : Int
  mut bytes : Int
} derive(Eq, Debug)

///|
pub fn FileTransferMetrics::new() -> FileTransferMetrics {
  { sessions: 0, completed: 0, failed: 0, cancelled: 0, segments: 0, bytes: 0 }
}

///|
pub fn FileTransferMetrics::sessions(self : FileTransferMetrics) -> Int {
  self.sessions
}

///|
pub fn FileTransferMetrics::completed(self : FileTransferMetrics) -> Int {
  self.completed
}

///|
pub fn FileTransferMetrics::failed(self : FileTransferMetrics) -> Int {
  self.failed
}

///|
pub fn FileTransferMetrics::cancelled(self : FileTransferMetrics) -> Int {
  self.cancelled
}

///|
pub fn FileTransferMetrics::segments(self : FileTransferMetrics) -> Int {
  self.segments
}

///|
pub fn FileTransferMetrics::bytes(self : FileTransferMetrics) -> Int {
  self.bytes
}

///|
/// File service coordinator for stores and station sessions.
pub struct FileTransferManager {
  store : FileStore
  sessions : Map[Int, FileTransferSession]
  history : Array[FileTransferSession]
  history_limit : Int
  mut next_ticket : Int
  metrics : FileTransferMetrics
} derive(Debug)

///|
pub fn FileTransferManager::new(
  store : FileStore,
  history_limit? : Int = 1024,
) -> Result[FileTransferManager, String] {
  if history_limit < 1 {
    Err("file transfer history limit must be positive")
  } else {
    Ok({
      store,
      sessions: {},
      history: [],
      history_limit,
      next_ticket: 1,
      metrics: FileTransferMetrics::new(),
    })
  }
}

///|
pub fn FileTransferManager::store(self : FileTransferManager) -> FileStore {
  self.store
}

///|
pub fn FileTransferManager::metrics(
  self : FileTransferManager,
) -> FileTransferMetrics {
  self.metrics
}

///|
pub fn FileTransferManager::session_count(self : FileTransferManager) -> Int {
  self.sessions.length()
}

///|
pub fn FileTransferManager::history(
  self : FileTransferManager,
) -> Array[FileTransferSession] {
  self.history.copy()
}

///|
pub fn FileTransferManager::get(
  self : FileTransferManager,
  ticket : Int,
) -> FileTransferSession? {
  self.sessions.get(ticket)
}

///|
pub fn FileTransferManager::begin_upload(
  self : FileTransferManager,
  identity : FileIdentity,
  expected_size : Int,
  expected_checksum? : UInt,
  segment_size? : Int = 240,
  timestamp? : Int = 0,
) -> Result[FileTransferSession, String] {
  match
    FileTransferSession::new(
      self.next_ticket,
      identity,
      UploadFile,
      expected_size,
      segment_size,
      expected_checksum,
      timestamp,
    ) {
    Err(error) => Err(error)
    Ok(session) => {
      self.sessions[session.ticket()] = session
      self.next_ticket += 1
      self.metrics.sessions += 1
      Ok(session)
    }
  }
}

///|
pub fn FileTransferManager::begin_download(
  self : FileTransferManager,
  identity : FileIdentity,
  segment_size? : Int = 240,
  timestamp? : Int = 0,
) -> Result[Array[FileSegment], String] {
  ignore(timestamp)
  match self.store.get(identity.name()) {
    None => Err("file does not exist")
    Some(file) => split_file_segments(file, segment_size)
  }
}

///|
pub fn FileTransferManager::activate(
  self : FileTransferManager,
  ticket : Int,
  timestamp : Int,
) -> Result[Unit, String] {
  match self.sessions.get(ticket) {
    None => Err("unknown file session")
    Some(session) =>
      match session.activate(timestamp) {
        Ok(_) => {
          self.sessions[ticket] = session
          Ok(())
        }
        Err(error) => Err(error)
      }
  }
}

///|
pub fn FileTransferManager::append(
  self : FileTransferManager,
  ticket : Int,
  segment : FileSegment,
  timestamp : Int,
) -> Result[FileTransferState, String] {
  match self.sessions.get(ticket) {
    None => Err("unknown file session")
    Some(session) =>
      match session.append(segment, timestamp) {
        Err(error) => {
          session.fail(timestamp, error)
          self.sessions[ticket] = session
          self.metrics.failed += 1
          Err(session.error())
        }
        Ok(_) => {
          self.sessions[ticket] = session
          self.metrics.segments += 1
          self.metrics.bytes += segment.data().length()
          if session.state() == FileCompleted {
            self.metrics.completed += 1
          }
          Ok(session.state())
        }
      }
  }
}

///|
pub fn FileTransferManager::ack(
  self : FileTransferManager,
  ticket : Int,
  timestamp : Int,
) -> Result[Unit, String] {
  match self.sessions.get(ticket) {
    None => Err("unknown file session")
    Some(session) =>
      match session.ack(timestamp) {
        Ok(_) => {
          self.sessions[ticket] = session
          Ok(())
        }
        Err(error) => Err(error)
      }
  }
}

///|
pub fn FileTransferManager::finish(
  self : FileTransferManager,
  ticket : Int,
  timestamp : Int,
) -> Result[StoredFile, String] {
  match self.sessions.get(ticket) {
    None => Err("unknown file session")
    Some(session) =>
      if session.state() != FileCompleted {
        Err("file session is not complete")
      } else {
        match self.store.put(session.identity(), session.data(), timestamp) {
          Ok(file) => {
            self.sessions.remove(ticket)
            self.history.push(session)
            while self.history.length() > self.history_limit {
              ignore(self.history.remove(0))
            }
            Ok(file)
          }
          Err(error) => Err(error)
        }
      }
  }
}

///|
pub fn FileTransferManager::cancel(
  self : FileTransferManager,
  ticket : Int,
  timestamp : Int,
) -> Result[Unit, String] {
  match self.sessions.get(ticket) {
    None => Err("unknown file session")
    Some(session) =>
      match session.cancel(timestamp) {
        Ok(_) => {
          self.sessions.remove(ticket)
          self.history.push(session)
          self.metrics.cancelled += 1
          Ok(())
        }
        Err(error) => Err(error)
      }
  }
}

///|
pub fn file_transfer_state_examples() -> Array[FileTransferState] {
  [
    FileIdle,
    FileSelecting,
    FileTransferring,
    FileWaitingForAck,
    FileCompleted,
    FileFailed,
    FileCancelled,
  ]
}

///|
pub fn file_transfer_direction_examples() -> Array[FileTransferDirection] {
  [UploadFile, DownloadFile]
}