// Public API implementation for the TELNET core.

///|
pub(all) struct Parser {
  config : ParserConfig
  checkpoint : ParserCheckpoint
  buffered_data : Bytes
  buffered_subnegotiation : Bytes
  pending_cr : Bool
  discarding_subnegotiation : Bool
} derive(Eq, Debug)

///|
pub(all) struct ParserFeedResult {
  parser : Parser
  events : Array[Event]
  bytes_consumed : Int
} derive(Eq, Debug)

///|
pub(all) struct ParserFinishResult {
  parser : Parser
  events : Array[Event]
  complete : Bool
} derive(Eq, Debug)

///|
fn b(n : Int) -> Byte {
  n.to_byte()
}

///|
fn unsafe_bytes_to_fixedarray(bytes : Bytes) -> FixedArray[Byte] = "%identity"

///|
fn setb(bytes : Bytes, index : Int, value : Byte) -> Unit {
  unsafe_bytes_to_fixedarray(bytes)[index] = value
}

///|
fn span_from_array(xs : Array[Byte]) -> ByteSpan {
  let bs = Bytes::from_array(xs)
  ByteSpan::{ bytes: bs, start: 0, length: bs.length() }
}

///|
fn empty_checkpoint() -> ParserCheckpoint {
  ParserCheckpoint::{
    mode: ParserMode::Normal,
    absolute_offset: 0,
    buffered_data_length: 0,
    buffered_subnegotiation_length: 0,
  }
}

///|
pub fn Parser::default_config() -> ParserConfig {
  ParserConfig::{
    max_data_chunk_bytes: 8192,
    max_subnegotiation_bytes: 8192,
    cr_policy: CrPolicy::Preserve,
    data_coalescing: DataCoalescing::CoalesceUntilCommand,
    strict_commands: true,
  }
}

///|
pub fn Parser::new(config : ParserConfig) -> Parser {
  let fixed = ParserConfig::{
    max_data_chunk_bytes: if config.max_data_chunk_bytes < 0 {
      0
    } else {
      config.max_data_chunk_bytes
    },
    max_subnegotiation_bytes: if config.max_subnegotiation_bytes < 0 {
      0
    } else {
      config.max_subnegotiation_bytes
    },
    cr_policy: config.cr_policy,
    data_coalescing: config.data_coalescing,
    strict_commands: config.strict_commands,
  }
  Parser::{
    config: fixed,
    checkpoint: empty_checkpoint(),
    buffered_data: Bytes::new(0),
    buffered_subnegotiation: Bytes::new(0),
    pending_cr: false,
    discarding_subnegotiation: false,
  }
}

///|
pub fn Parser::default() -> Parser {
  Parser::new(Parser::default_config())
}

///|
fn err_event(
  kind : TelnetErrorKind,
  offset : Int,
  byte : Byte?,
  option : OptionCode?,
) -> Event {
  Event::Error(TelnetError::{ kind, offset, byte, option })
}

///|
fn data_event(xs : Array[Byte]) -> Event {
  Event::Data(DataEvent::{ span: span_from_array(xs) })
}

///|
fn flush_data(data : Array[Byte], events : Array[Event]) -> Unit {
  if data.length() > 0 {
    events.push(data_event(data.copy()))
    data.clear()
  }
}

///|
fn append_span_to_array(out : Array[Byte], span : ByteSpan) -> Unit {
  for i = 0; i < span.length; i = i + 1 {
    out.push(span.bytes[span.start + i])
  }
}

///|
pub fn Parser::feed(self : Parser, chunk : Bytes) -> ParserFeedResult {
  self.feed_span(ByteSpan::{ bytes: chunk, start: 0, length: chunk.length() })
}

///|
pub fn Parser::feed_span(self : Parser, span : ByteSpan) -> ParserFeedResult {
  self.feed_span_with_events(span, Array::new())
}

///|
pub fn Parser::feed_span_into(
  self : Parser,
  span : ByteSpan,
  events : Array[Event],
) -> ParserFeedResult {
  events.clear()
  self.feed_span_with_events(span, events)
}

///|
#cfg(target="native")
#borrow(bytes)
extern "C" fn find_byte_native(
  bytes : Bytes,
  start : Int,
  length : Int,
  needle : Int,
) -> Int = "moonbit_telnet_find_byte"

///|
#cfg(not(target="native"))
fn find_byte_native(
  bytes : Bytes,
  start : Int,
  length : Int,
  needle : Int,
) -> Int {
  let target = b(needle)
  let mut i = 0
  while i < length {
    if bytes[start + i] == target {
      return i
    }
    i = i + 1
  }
  -1
}

///|
fn span_has_iac(span : ByteSpan) -> Bool {
  find_byte_native(span.bytes, span.start, span.length, 255) >= 0
}

///|
fn push_preserved_data_events(
  events : Array[Event],
  span : ByteSpan,
  max_chunk_bytes : Int,
  coalescing : DataCoalescing,
  borrow_full_span : Bool,
) -> Unit {
  let chunk_size = if coalescing == DataCoalescing::EmitImmediately {
    1
  } else {
    max_chunk_bytes
  }
  let mut consumed = 0
  while consumed < span.length {
    let remaining = span.length - consumed
    let n = if remaining < chunk_size { remaining } else { chunk_size }
    let event_span = if borrow_full_span && consumed == 0 && n == span.length {
      ByteSpan::{ bytes: span.bytes, start: span.start, length: n }
    } else {
      ByteSpan::{
        bytes: ByteSpan::{
          bytes: span.bytes,
          start: span.start + consumed,
          length: n,
        }.to_bytes(),
        start: 0,
        length: n,
      }
    }
    events.push(Event::Data(DataEvent::{ span: event_span }))
    consumed = consumed + n
  }
}

///|
fn can_use_preserve_data_fast_path(parser : Parser, span : ByteSpan) -> Bool {
  parser.checkpoint.mode == ParserMode::Normal &&
  !parser.pending_cr &&
  !parser.discarding_subnegotiation &&
  parser.buffered_data.length() == 0 &&
  parser.buffered_subnegotiation.length() == 0 &&
  parser.config.max_data_chunk_bytes > 0 &&
  parser.config.cr_policy == CrPolicy::Preserve &&
  !span_has_iac(span)
}

///|
fn Parser::feed_span_with_events(
  self : Parser,
  span : ByteSpan,
  events : Array[Event],
) -> ParserFeedResult {
  if span.length == 0 {
    return ParserFeedResult::{ parser: self, events, bytes_consumed: 0 }
  }
  if can_use_preserve_data_fast_path(self, span) {
    push_preserved_data_events(
      events,
      span,
      self.config.max_data_chunk_bytes,
      self.config.data_coalescing,
      true,
    )
    return ParserFeedResult::{
      parser: Parser::{
        config: self.config,
        checkpoint: ParserCheckpoint::{
          mode: ParserMode::Normal,
          absolute_offset: self.checkpoint.absolute_offset + span.length,
          buffered_data_length: 0,
          buffered_subnegotiation_length: 0,
        },
        buffered_data: Bytes::new(0),
        buffered_subnegotiation: Bytes::new(0),
        pending_cr: false,
        discarding_subnegotiation: false,
      },
      events,
      bytes_consumed: span.length,
    }
  }
  let data = if self.pending_cr {
    Array::new()
  } else {
    self.buffered_data.to_array()
  }
  let sub = self.buffered_subnegotiation.to_array()
  let mut mode = self.checkpoint.mode
  let mut discarding_subnegotiation = self.discarding_subnegotiation
  let mut i = 0
  if self.pending_cr {
    let byte = span.bytes[span.start]
    if self.config.cr_policy == CrPolicy::ValidateNvt {
      if byte != b(10) && byte != b(0) {
        events.push(
          err_event(
            TelnetErrorKind::PolicyRejected,
            self.checkpoint.absolute_offset,
            Some(byte),
            None,
          ),
        )
      } else {
        data.push(b(13))
        data.push(byte)
        flush_data(data, events)
        i = 1
      }
    } else if self.config.cr_policy == CrPolicy::NormalizeToLf && byte == b(10) {
      data.push(b(10))
      flush_data(data, events)
      i = 1
    } else if self.config.cr_policy == CrPolicy::NormalizeToLf && byte == b(0) {
      data.push(b(13))
      flush_data(data, events)
      i = 1
    } else if self.config.cr_policy == CrPolicy::NormalizeToLf {
      data.push(b(13))
    }
  }
  while i < span.length {
    let absolute = self.checkpoint.absolute_offset + i
    let byte = span.bytes[span.start + i]
    match mode {
      ParserMode::Normal =>
        if byte == b(255) {
          flush_data(data, events)
          mode = ParserMode::AfterIac
        } else if self.config.cr_policy == CrPolicy::Preserve &&
          self.config.max_data_chunk_bytes > 0 {
          let run_start = i
          let next_iac = find_byte_native(
            span.bytes,
            span.start + i,
            span.length - i,
            255,
          )
          let run_end = if next_iac < 0 { span.length } else { i + next_iac }
          push_preserved_data_events(
            events,
            ByteSpan::{
              bytes: span.bytes,
              start: span.start + run_start,
              length: run_end - run_start,
            },
            self.config.max_data_chunk_bytes,
            self.config.data_coalescing,
            false,
          )
          i = run_end - 1
        } else if self.config.max_data_chunk_bytes == 0 {
          events.push(
            err_event(
              TelnetErrorKind::PolicyRejected,
              absolute,
              Some(byte),
              None,
            ),
          )
        } else if self.config.cr_policy == CrPolicy::ValidateNvt &&
          byte == b(13) &&
          i + 1 == span.length {
          flush_data(data, events)
          data.push(byte)
        } else if self.config.cr_policy == CrPolicy::ValidateNvt &&
          byte == b(13) &&
          i + 1 < span.length &&
          span.bytes[span.start + i + 1] != b(10) &&
          span.bytes[span.start + i + 1] != b(0) {
          flush_data(data, events)
          events.push(
            err_event(
              TelnetErrorKind::PolicyRejected,
              absolute + 1,
              Some(span.bytes[span.start + i + 1]),
              None,
            ),
          )
        } else if self.config.cr_policy == CrPolicy::NormalizeToLf &&
          byte == b(13) &&
          i + 1 == span.length {
          flush_data(data, events)
          data.push(byte)
        } else if self.config.cr_policy == CrPolicy::NormalizeToLf &&
          byte == b(13) &&
          i + 1 < span.length &&
          span.bytes[span.start + i + 1] == b(10) {
          data.push(b(10))
          i = i + 1
          flush_data(data, events)
        } else if self.config.cr_policy == CrPolicy::NormalizeToLf &&
          byte == b(13) &&
          i + 1 < span.length &&
          span.bytes[span.start + i + 1] == b(0) {
          data.push(b(13))
          i = i + 1
          flush_data(data, events)
        } else if self.config.cr_policy == CrPolicy::ValidateNvt &&
          byte == b(13) &&
          data.length() > 0 {
          flush_data(data, events)
          data.push(byte)
        } else {
          data.push(byte)
          if self.config.data_coalescing == DataCoalescing::EmitImmediately ||
            data.length() >= self.config.max_data_chunk_bytes {
            flush_data(data, events)
          }
        }
      ParserMode::AfterIac =>
        if byte == b(255) {
          data.push(b(255))
          flush_data(data, events)
          mode = ParserMode::Normal
        } else {
          match Command::from_byte(byte) {
            Some(Command::WILL) =>
              mode = ParserMode::AfterNegotiationVerb(NegotiationVerb::Will)
            Some(Command::WONT) =>
              mode = ParserMode::AfterNegotiationVerb(NegotiationVerb::Wont)
            Some(Command::DO) =>
              mode = ParserMode::AfterNegotiationVerb(NegotiationVerb::Do)
            Some(Command::DONT) =>
              mode = ParserMode::AfterNegotiationVerb(NegotiationVerb::Dont)
            Some(Command::SB) => mode = ParserMode::AfterSubnegotiationStart
            Some(Command::SE) => {
              events.push(
                err_event(
                  TelnetErrorKind::UnexpectedSubnegotiationEnd,
                  absolute,
                  Some(byte),
                  None,
                ),
              )
              mode = ParserMode::Normal
            }
            Some(c) => {
              events.push(Event::Command(CommandEvent::{ command: c }))
              mode = ParserMode::Normal
            }
            None => {
              events.push(
                err_event(
                  TelnetErrorKind::InvalidCommandByte,
                  absolute,
                  Some(byte),
                  None,
                ),
              )
              mode = ParserMode::Normal
            }
          }
        }
      ParserMode::AfterNegotiationVerb(v) => {
        events.push(
          Event::Negotiation(NegotiationEvent::{
            verb: v,
            option: OptionCode::{ value: byte },
          }),
        )
        mode = ParserMode::Normal
      }
      ParserMode::AfterSubnegotiationStart => {
        sub.clear()
        mode = ParserMode::InSubnegotiation(OptionCode::{ value: byte })
      }
      ParserMode::InSubnegotiation(opt) =>
        if byte == b(255) {
          mode = ParserMode::InSubnegotiationAfterIac(opt)
        } else {
          let run_start = i
          let next_iac = find_byte_native(
            span.bytes,
            span.start + i,
            span.length - i,
            255,
          )
          let run_end = if next_iac < 0 { span.length } else { i + next_iac }
          if !discarding_subnegotiation {
            let run_len = run_end - run_start
            let remaining_capacity = self.config.max_subnegotiation_bytes -
              sub.length()
            if run_len > remaining_capacity {
              let overflow_index = if remaining_capacity < 0 {
                0
              } else {
                remaining_capacity
              }
              if overflow_index >= 0 {
                append_span_to_array(sub, ByteSpan::{
                  bytes: span.bytes,
                  start: span.start + run_start,
                  length: overflow_index + 1,
                })
              }
              events.push(
                err_event(
                  TelnetErrorKind::SubnegotiationTooLarge,
                  absolute + overflow_index,
                  Some(span.bytes[span.start + run_start + overflow_index]),
                  None,
                ),
              )
              sub.clear()
              discarding_subnegotiation = true
            } else {
              append_span_to_array(sub, ByteSpan::{
                bytes: span.bytes,
                start: span.start + run_start,
                length: run_len,
              })
            }
          }
          i = run_end - 1
        }
      ParserMode::InSubnegotiationAfterIac(opt) =>
        if byte == b(255) {
          if !discarding_subnegotiation {
            sub.push(b(255))
            if sub.length() > self.config.max_subnegotiation_bytes {
              events.push(
                err_event(
                  TelnetErrorKind::SubnegotiationTooLarge,
                  absolute,
                  Some(byte),
                  None,
                ),
              )
              sub.clear()
              discarding_subnegotiation = true
            }
          }
          mode = ParserMode::InSubnegotiation(opt)
        } else if byte == b(240) {
          if !discarding_subnegotiation {
            events.push(
              Event::Subnegotiation(SubnegotiationEvent::{
                option: opt,
                payload: span_from_array(sub.copy()),
              }),
            )
          }
          sub.clear()
          discarding_subnegotiation = false
          mode = ParserMode::Normal
        } else {
          let err_opt = if self.config.cr_policy == CrPolicy::ValidateNvt {
            Some(opt)
          } else {
            None
          }
          events.push(
            err_event(
              TelnetErrorKind::InvalidCommandByte,
              absolute,
              Some(byte),
              err_opt,
            ),
          )
          mode = ParserMode::InSubnegotiation(opt)
        }
    }
    i = i + 1
  }
  let pending_cr = mode == ParserMode::Normal &&
    data.length() == 1 &&
    data[0] == b(13) &&
    (
      self.config.cr_policy == CrPolicy::ValidateNvt ||
      self.config.cr_policy == CrPolicy::NormalizeToLf
    )
  if !pending_cr {
    flush_data(data, events)
  }
  let cp = ParserCheckpoint::{
    mode,
    absolute_offset: self.checkpoint.absolute_offset + span.length,
    buffered_data_length: data.length(),
    buffered_subnegotiation_length: sub.length(),
  }
  ParserFeedResult::{
    parser: Parser::{
      config: self.config,
      checkpoint: cp,
      buffered_data: if pending_cr {
        Bytes::from_array(data)
      } else {
        Bytes::new(0)
      },
      buffered_subnegotiation: Bytes::from_array(sub),
      pending_cr,
      discarding_subnegotiation,
    },
    events,
    bytes_consumed: span.length,
  }
}

///|
pub fn Parser::finish(self : Parser) -> ParserFinishResult {
  let events = Array::new()
  let offset = if self.checkpoint.absolute_offset > 0 {
    self.checkpoint.absolute_offset - 1
  } else {
    0
  }
  match self.checkpoint.mode {
    ParserMode::Normal =>
      if self.pending_cr {
        events.push(
          err_event(TelnetErrorKind::PolicyRejected, offset, Some(b(13)), None),
        )
      }
    ParserMode::AfterIac =>
      events.push(
        err_event(TelnetErrorKind::IncompleteIac, offset, Some(b(255)), None),
      )
    ParserMode::AfterNegotiationVerb(v) =>
      events.push(
        err_event(
          TelnetErrorKind::IncompleteNegotiation,
          offset,
          Some(v.to_command().to_byte()),
          None,
        ),
      )
    ParserMode::AfterSubnegotiationStart
    | ParserMode::InSubnegotiation(_)
    | ParserMode::InSubnegotiationAfterIac(_) =>
      if !self.discarding_subnegotiation {
        events.push(
          err_event(
            TelnetErrorKind::IncompleteSubnegotiation,
            offset,
            if self.buffered_subnegotiation.length() > 0 {
              Some(
                self.buffered_subnegotiation[self.buffered_subnegotiation.length() -
                1],
              )
            } else {
              None
            },
            None,
          ),
        )
      }
  }
  ParserFinishResult::{
    parser: self.reset(),
    events,
    complete: events.length() == 0,
  }
}

///|
pub fn Parser::checkpoint(self : Parser) -> ParserCheckpoint {
  self.checkpoint
}

///|
pub fn Parser::restore(
  config : ParserConfig,
  checkpoint : ParserCheckpoint,
) -> Parser {
  Parser::{
    config,
    checkpoint,
    buffered_data: Bytes::new(0),
    buffered_subnegotiation: Bytes::new(0),
    pending_cr: false,
    discarding_subnegotiation: false,
  }
}

///|
pub fn Parser::reset(self : Parser) -> Parser {
  Parser::{
    config: self.config,
    checkpoint: empty_checkpoint(),
    buffered_data: Bytes::new(0),
    buffered_subnegotiation: Bytes::new(0),
    pending_cr: false,
    discarding_subnegotiation: false,
  }
}

///|
pub fn Parser::with_config(self : Parser, config : ParserConfig) -> Parser {
  Parser::{
    config,
    checkpoint: self.checkpoint,
    buffered_data: self.buffered_data,
    buffered_subnegotiation: self.buffered_subnegotiation,
    pending_cr: self.pending_cr,
    discarding_subnegotiation: self.discarding_subnegotiation,
  }
}

///|
pub(all) struct Encoder {
  canonical : Bool
} derive(Eq, Debug)

///|
pub(all) struct EncodeResult {
  bytes_written : Int
  required_capacity : Int
} derive(Eq, Debug)

///|
pub fn Encoder::new() -> Encoder {
  Encoder::{ canonical: false }
}

///|
pub fn Encoder::canonical() -> Encoder {
  Encoder::{ canonical: true }
}

///|
fn span_required(span : ByteSpan, escaped : Bool) -> Int {
  let mut n = 0
  for i = 0; i < span.length; i = i + 1 {
    n = n + 1
    if escaped && span.bytes[span.start + i] == b(255) {
      n = n + 1
    }
  }
  n
}

///|
pub fn Encoder::required_capacity(_self : Encoder, item : EncodeItem) -> Int {
  match item {
    EncodeItem::RawData(s) => s.length
    EncodeItem::EscapedData(s) => span_required(s, true)
    EncodeItem::Command(_) => 2
    EncodeItem::Negotiation(_) => 3
    EncodeItem::Subnegotiation(e) => 5 + span_required(e.payload, true)
  }
}

///|
fn output_too_small(
  required : Int,
  output : Bytes,
) -> Result[EncodeResult, EncodeError]? {
  if output.length() < required {
    Some(
      Err(EncodeError::{
        kind: EncodeErrorKind::OutputBufferTooSmall,
        offset: 0,
        required_capacity: required,
      }),
    )
  } else {
    None
  }
}

///|
pub fn Encoder::encode_item(
  self : Encoder,
  item : EncodeItem,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  match item {
    EncodeItem::RawData(s) => self.encode_data(s, output)
    EncodeItem::EscapedData(s) => self.encode_data(s, output)
    EncodeItem::Command(c) => self.encode_command(c, output)
    EncodeItem::Negotiation(e) => self.encode_negotiation(e, output)
    EncodeItem::Subnegotiation(e) => self.encode_subnegotiation(e, output)
  }
}

///|
pub fn Encoder::encode_data(
  self : Encoder,
  data : ByteSpan,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  if data.start < 0 ||
    data.length < 0 ||
    data.start + data.length > data.bytes.length() {
    return Err(EncodeError::{
      kind: EncodeErrorKind::UnsupportedEncoding,
      offset: 0,
      required_capacity: 0,
    })
  }
  if !self.canonical {
    match output_too_small(data.length, output) {
      Some(e) => return e
      None => ()
    }
    FixedArray::blit_from_bytes(
      unsafe_bytes_to_fixedarray(output),
      0,
      data.bytes,
      data.start,
      data.length,
    )
    return Ok(EncodeResult::{
      bytes_written: data.length,
      required_capacity: data.length,
    })
  }
  let required = span_required(data, true)
  match output_too_small(required, output) {
    Some(e) => return e
    None => ()
  }
  let mut j = 0
  for i = 0; i < data.length; i = i + 1 {
    let x = data.bytes[data.start + i]
    setb(output, j, x)
    j = j + 1
    if x == b(255) {
      setb(output, j, b(255))
      j = j + 1
    }
  }
  Ok(EncodeResult::{ bytes_written: required, required_capacity: required })
}

///|
pub fn Encoder::encode_data_assume_capacity(
  self : Encoder,
  data : ByteSpan,
  output : Bytes,
  required : Int,
) -> Result[EncodeResult, EncodeError] {
  if data.start < 0 ||
    data.length < 0 ||
    data.start + data.length > data.bytes.length() {
    return Err(EncodeError::{
      kind: EncodeErrorKind::UnsupportedEncoding,
      offset: 0,
      required_capacity: 0,
    })
  }
  if required < 0 {
    return Err(EncodeError::{
      kind: EncodeErrorKind::OutputBufferTooSmall,
      offset: 0,
      required_capacity: required,
    })
  }
  match output_too_small(required, output) {
    Some(e) => return e
    None => ()
  }
  if !self.canonical {
    FixedArray::blit_from_bytes(
      unsafe_bytes_to_fixedarray(output),
      0,
      data.bytes,
      data.start,
      data.length,
    )
    return Ok(EncodeResult::{
      bytes_written: required,
      required_capacity: required,
    })
  }
  let mut j = 0
  for i = 0; i < data.length; i = i + 1 {
    let x = data.bytes[data.start + i]
    setb(output, j, x)
    j = j + 1
    if x == b(255) {
      setb(output, j, b(255))
      j = j + 1
    }
  }
  Ok(EncodeResult::{ bytes_written: required, required_capacity: required })
}

///|
pub fn Encoder::encode_command(
  _self : Encoder,
  command : Command,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  match command {
    Command::SE
    | Command::IAC
    | Command::SB
    | Command::WILL
    | Command::WONT
    | Command::DO
    | Command::DONT =>
      return Err(EncodeError::{
        kind: EncodeErrorKind::UnsupportedEncoding,
        offset: 0,
        required_capacity: 0,
      })
    _ => ()
  }
  match output_too_small(2, output) {
    Some(e) => return e
    None => ()
  }
  setb(output, 0, b(255))
  setb(output, 1, command.to_byte())
  Ok(EncodeResult::{ bytes_written: 2, required_capacity: 2 })
}

///|
pub fn Encoder::encode_negotiation(
  _self : Encoder,
  event : NegotiationEvent,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  match output_too_small(3, output) {
    Some(e) => return e
    None => ()
  }
  setb(output, 0, b(255))
  setb(output, 1, event.verb.to_command().to_byte())
  setb(output, 2, event.option.value)
  Ok(EncodeResult::{ bytes_written: 3, required_capacity: 3 })
}

///|
pub fn Encoder::encode_subnegotiation(
  _self : Encoder,
  event : SubnegotiationEvent,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  let required = 5 + span_required(event.payload, true)
  match output_too_small(required, output) {
    Some(e) => return e
    None => ()
  }
  setb(output, 0, b(255))
  setb(output, 1, b(250))
  setb(output, 2, event.option.value)
  let mut j = 3
  for i = 0; i < event.payload.length; i = i + 1 {
    let x = event.payload.bytes[event.payload.start + i]
    setb(output, j, x)
    j = j + 1
    if x == b(255) {
      setb(output, j, b(255))
      j = j + 1
    }
  }
  setb(output, j, b(255))
  setb(output, j + 1, b(240))
  Ok(EncodeResult::{ bytes_written: required, required_capacity: required })
}

///|
pub fn Encoder::encode_subnegotiation_assume_capacity(
  _self : Encoder,
  event : SubnegotiationEvent,
  output : Bytes,
  required : Int,
) -> Result[EncodeResult, EncodeError] {
  if required < 0 {
    return Err(EncodeError::{
      kind: EncodeErrorKind::OutputBufferTooSmall,
      offset: 0,
      required_capacity: required,
    })
  }
  match output_too_small(required, output) {
    Some(e) => return e
    None => ()
  }
  setb(output, 0, b(255))
  setb(output, 1, b(250))
  setb(output, 2, event.option.value)
  let mut j = 3
  for i = 0; i < event.payload.length; i = i + 1 {
    let x = event.payload.bytes[event.payload.start + i]
    setb(output, j, x)
    j = j + 1
    if x == b(255) {
      setb(output, j, b(255))
      j = j + 1
    }
  }
  setb(output, j, b(255))
  setb(output, j + 1, b(240))
  Ok(EncodeResult::{ bytes_written: required, required_capacity: required })
}

///|
pub(all) struct Negotiator {
  states : Array[OptionState]
} derive(Eq, Debug)

///|
pub fn Negotiator::new() -> Negotiator {
  Negotiator::{ states: [] }
}

///|
pub fn Negotiator::with_states(states : Array[OptionState]) -> Negotiator {
  Negotiator::{ states, }
}

///|
pub fn Negotiator::state_for(
  self : Negotiator,
  option : OptionCode,
) -> OptionState {
  for s in self.states {
    if s.option == option {
      return s
    }
  }
  OptionState::{
    option,
    local_state: HalfState::No,
    remote_state: HalfState::No,
  }
}

///|
pub fn Negotiator::receive(
  self : Negotiator,
  input : NegotiationEvent,
  decision : PolicyDecision,
) -> NegotiationTransition {
  let before = self.state_for(input.option)
  let mut after = before
  let actions = Array::new()
  match input.verb {
    NegotiationVerb::Will =>
      if decision == PolicyDecision::Accept &&
        before.remote_state != HalfState::Yes {
        after = OptionState::{
          option: input.option,
          local_state: before.local_state,
          remote_state: HalfState::Yes,
        }
        actions.push(
          NegotiationAction::Send(NegotiationEvent::{
            verb: NegotiationVerb::Do,
            option: input.option,
          }),
        )
        actions.push(
          NegotiationAction::MarkEnabled(OptionSide::Remote, input.option),
        )
      } else if decision == PolicyDecision::Reject {
        actions.push(
          NegotiationAction::Send(NegotiationEvent::{
            verb: NegotiationVerb::Dont,
            option: input.option,
          }),
        )
      } else {
        actions.push(NegotiationAction::NoAction)
      }
    NegotiationVerb::Wont => {
      after = OptionState::{
        option: input.option,
        local_state: before.local_state,
        remote_state: HalfState::No,
      }
      actions.push(
        NegotiationAction::MarkDisabled(OptionSide::Remote, input.option),
      )
    }
    NegotiationVerb::Do =>
      if decision == PolicyDecision::Accept &&
        before.local_state != HalfState::Yes {
        after = OptionState::{
          option: input.option,
          local_state: HalfState::Yes,
          remote_state: before.remote_state,
        }
        actions.push(
          NegotiationAction::Send(NegotiationEvent::{
            verb: NegotiationVerb::Will,
            option: input.option,
          }),
        )
        actions.push(
          NegotiationAction::MarkEnabled(OptionSide::Local, input.option),
        )
      } else if decision == PolicyDecision::Reject {
        actions.push(
          NegotiationAction::Send(NegotiationEvent::{
            verb: NegotiationVerb::Wont,
            option: input.option,
          }),
        )
      } else {
        actions.push(NegotiationAction::NoAction)
      }
    NegotiationVerb::Dont => {
      after = OptionState::{
        option: input.option,
        local_state: HalfState::No,
        remote_state: before.remote_state,
      }
      actions.push(
        NegotiationAction::Send(NegotiationEvent::{
          verb: NegotiationVerb::Wont,
          option: input.option,
        }),
      )
      actions.push(
        NegotiationAction::MarkDisabled(OptionSide::Local, input.option),
      )
    }
  }
  NegotiationTransition::{ before, input, decision, after, actions }
}

///|
pub fn Negotiator::request(
  self : Negotiator,
  option : OptionCode,
  side : OptionSide,
  enable : Bool,
) -> NegotiationTransition {
  let before = self.state_for(option)
  let verb = match (side, enable) {
    (OptionSide::Remote, true) => NegotiationVerb::Do
    (OptionSide::Remote, false) => NegotiationVerb::Dont
    (OptionSide::Local, true) => NegotiationVerb::Will
    (OptionSide::Local, false) => NegotiationVerb::Wont
  }
  let current = match side {
    OptionSide::Local => before.local_state
    OptionSide::Remote => before.remote_state
  }
  let (after, actions) = match (side, enable, current) {
    (OptionSide::Local, true, HalfState::Yes)
    | (OptionSide::Remote, true, HalfState::Yes) =>
      (before, [NegotiationAction::NoAction])
    (OptionSide::Local, false, HalfState::No)
    | (OptionSide::Remote, false, HalfState::No) =>
      (before, [NegotiationAction::NoAction])
    (OptionSide::Local, false, HalfState::WantYes(QueueBit::Empty)) =>
      (
        OptionState::{
          option,
          local_state: HalfState::WantYes(QueueBit::Opposite),
          remote_state: before.remote_state,
        },
        [NegotiationAction::NoAction],
      )
    (OptionSide::Remote, false, HalfState::WantYes(QueueBit::Empty)) =>
      (
        OptionState::{
          option,
          local_state: before.local_state,
          remote_state: HalfState::WantYes(QueueBit::Opposite),
        },
        [NegotiationAction::NoAction],
      )
    (OptionSide::Remote, true, _) =>
      (
        OptionState::{
          option,
          local_state: before.local_state,
          remote_state: HalfState::WantYes(QueueBit::Empty),
        },
        [NegotiationAction::Send(NegotiationEvent::{ verb, option })],
      )
    (OptionSide::Remote, false, _) =>
      (
        OptionState::{
          option,
          local_state: before.local_state,
          remote_state: HalfState::WantNo(QueueBit::Empty),
        },
        [NegotiationAction::Send(NegotiationEvent::{ verb, option })],
      )
    (OptionSide::Local, true, _) =>
      (
        OptionState::{
          option,
          local_state: HalfState::WantYes(QueueBit::Empty),
          remote_state: before.remote_state,
        },
        [NegotiationAction::Send(NegotiationEvent::{ verb, option })],
      )
    (OptionSide::Local, false, _) =>
      (
        OptionState::{
          option,
          local_state: HalfState::WantNo(QueueBit::Empty),
          remote_state: before.remote_state,
        },
        [NegotiationAction::Send(NegotiationEvent::{ verb, option })],
      )
  }
  NegotiationTransition::{
    before,
    input: NegotiationEvent::{ verb, option },
    decision: PolicyDecision::Accept,
    after,
    actions,
  }
}

///|
pub fn Negotiator::apply(
  self : Negotiator,
  transition : NegotiationTransition,
) -> Negotiator {
  if transition.after == transition.before {
    return self
  }
  if self.states.length() == 0 {
    return Negotiator::{ states: [transition.after] }
  }
  if self.states.length() == 1 {
    let state = self.states[0]
    if state.option == transition.after.option {
      return Negotiator::{ states: [transition.after] }
    } else {
      return Negotiator::{ states: [state, transition.after] }
    }
  }
  let out = Array::new()
  let mut found = false
  for s in self.states {
    if s.option == transition.after.option {
      out.push(transition.after)
      found = true
    } else {
      out.push(s)
    }
  }
  if !found {
    out.push(transition.after)
  }
  Negotiator::{ states: out }
}

///|
pub fn Command::from_byte(byte : Byte) -> Command? {
  match byte.to_int() {
    240 => Some(Command::SE)
    241 => Some(Command::NOP)
    242 => Some(Command::DM)
    243 => Some(Command::BRK)
    244 => Some(Command::IP)
    245 => Some(Command::AO)
    246 => Some(Command::AYT)
    247 => Some(Command::EC)
    248 => Some(Command::EL)
    249 => Some(Command::GA)
    250 => Some(Command::SB)
    251 => Some(Command::WILL)
    252 => Some(Command::WONT)
    253 => Some(Command::DO)
    254 => Some(Command::DONT)
    255 => Some(Command::IAC)
    _ => None
  }
}

///|
pub fn Command::to_byte(self : Command) -> Byte {
  match self {
    Command::SE => b(240)
    Command::NOP => b(241)
    Command::DM => b(242)
    Command::BRK => b(243)
    Command::IP => b(244)
    Command::AO => b(245)
    Command::AYT => b(246)
    Command::EC => b(247)
    Command::EL => b(248)
    Command::GA => b(249)
    Command::SB => b(250)
    Command::WILL => b(251)
    Command::WONT => b(252)
    Command::DO => b(253)
    Command::DONT => b(254)
    Command::IAC => b(255)
  }
}

///|
pub fn NegotiationVerb::from_command(command : Command) -> NegotiationVerb? {
  match command {
    Command::WILL => Some(NegotiationVerb::Will)
    Command::WONT => Some(NegotiationVerb::Wont)
    Command::DO => Some(NegotiationVerb::Do)
    Command::DONT => Some(NegotiationVerb::Dont)
    _ => None
  }
}

///|
pub fn NegotiationVerb::to_command(self : NegotiationVerb) -> Command {
  match self {
    NegotiationVerb::Will => Command::WILL
    NegotiationVerb::Wont => Command::WONT
    NegotiationVerb::Do => Command::DO
    NegotiationVerb::Dont => Command::DONT
  }
}

///|
pub fn OptionCode::new(byte : Byte) -> OptionCode {
  OptionCode::{ value: byte }
}

///|
pub fn OptionCode::to_byte(self : OptionCode) -> Byte {
  self.value
}

///|
pub fn KnownOption::from_code(code : OptionCode) -> KnownOption {
  match code.value.to_int() {
    0 => KnownOption::Binary
    1 => KnownOption::Echo
    2 => KnownOption::Reconnection
    3 => KnownOption::SuppressGoAhead
    4 => KnownOption::ApproxMessageSizeNegotiation
    5 => KnownOption::Status
    6 => KnownOption::TimingMark
    7 => KnownOption::RemoteControlledTransAndEcho
    8 => KnownOption::OutputLineWidth
    9 => KnownOption::OutputPageSize
    10 => KnownOption::OutputCarriageReturnDisposition
    11 => KnownOption::OutputHorizontalTabStops
    12 => KnownOption::OutputHorizontalTabDisposition
    13 => KnownOption::OutputFormfeedDisposition
    14 => KnownOption::OutputVerticalTabStops
    15 => KnownOption::OutputVerticalTabDisposition
    16 => KnownOption::OutputLinefeedDisposition
    17 => KnownOption::ExtendedAscii
    18 => KnownOption::Logout
    19 => KnownOption::ByteMacro
    20 => KnownOption::DataEntryTerminal
    21 => KnownOption::Supdup
    22 => KnownOption::SupdupOutput
    23 => KnownOption::SendLocation
    24 => KnownOption::TerminalType
    25 => KnownOption::EndOfRecord
    26 => KnownOption::TacacsUserIdentification
    27 => KnownOption::OutputMarking
    28 => KnownOption::TerminalLocationNumber
    29 => KnownOption::Telnet3270Regime
    30 => KnownOption::X3Pad
    31 => KnownOption::Naws
    32 => KnownOption::TerminalSpeed
    33 => KnownOption::RemoteFlowControl
    34 => KnownOption::Linemode
    35 => KnownOption::XDisplayLocation
    36 => KnownOption::Environment
    37 => KnownOption::Authentication
    38 => KnownOption::Encryption
    39 => KnownOption::NewEnvironment
    40 => KnownOption::Tn3270e
    41 => KnownOption::Xauth
    42 => KnownOption::Charset
    43 => KnownOption::TelnetRemoteSerialPort
    44 => KnownOption::ComPortControl
    45 => KnownOption::TelnetSuppressLocalEcho
    46 => KnownOption::TelnetStartTls
    47 => KnownOption::Kermit
    48 => KnownOption::SendUrl
    49 => KnownOption::ForwardX
    255 => KnownOption::ExtendedOptionsList
    _ => KnownOption::Unknown(code)
  }
}

///|
pub fn KnownOption::to_code(self : KnownOption) -> OptionCode {
  let n = match self {
    KnownOption::Binary => 0
    KnownOption::Echo => 1
    KnownOption::Reconnection => 2
    KnownOption::SuppressGoAhead => 3
    KnownOption::ApproxMessageSizeNegotiation => 4
    KnownOption::Status => 5
    KnownOption::TimingMark => 6
    KnownOption::RemoteControlledTransAndEcho => 7
    KnownOption::OutputLineWidth => 8
    KnownOption::OutputPageSize => 9
    KnownOption::OutputCarriageReturnDisposition => 10
    KnownOption::OutputHorizontalTabStops => 11
    KnownOption::OutputHorizontalTabDisposition => 12
    KnownOption::OutputFormfeedDisposition => 13
    KnownOption::OutputVerticalTabStops => 14
    KnownOption::OutputVerticalTabDisposition => 15
    KnownOption::OutputLinefeedDisposition => 16
    KnownOption::ExtendedAscii => 17
    KnownOption::Logout => 18
    KnownOption::ByteMacro => 19
    KnownOption::DataEntryTerminal => 20
    KnownOption::Supdup => 21
    KnownOption::SupdupOutput => 22
    KnownOption::SendLocation => 23
    KnownOption::TerminalType => 24
    KnownOption::EndOfRecord => 25
    KnownOption::TacacsUserIdentification => 26
    KnownOption::OutputMarking => 27
    KnownOption::TerminalLocationNumber => 28
    KnownOption::Telnet3270Regime => 29
    KnownOption::X3Pad => 30
    KnownOption::Naws => 31
    KnownOption::TerminalSpeed => 32
    KnownOption::RemoteFlowControl => 33
    KnownOption::Linemode => 34
    KnownOption::XDisplayLocation => 35
    KnownOption::Environment => 36
    KnownOption::Authentication => 37
    KnownOption::Encryption => 38
    KnownOption::NewEnvironment => 39
    KnownOption::Tn3270e => 40
    KnownOption::Xauth => 41
    KnownOption::Charset => 42
    KnownOption::TelnetRemoteSerialPort => 43
    KnownOption::ComPortControl => 44
    KnownOption::TelnetSuppressLocalEcho => 45
    KnownOption::TelnetStartTls => 46
    KnownOption::Kermit => 47
    KnownOption::SendUrl => 48
    KnownOption::ForwardX => 49
    KnownOption::ExtendedOptionsList => 255
    KnownOption::Unknown(c) => return c
  }
  OptionCode::{ value: b(n) }
}

///|
pub fn ByteSpan::new(bytes : Bytes, start : Int, length : Int) -> ByteSpan {
  if start < 0 {
    ByteSpan::{ bytes, start: 0, length: 0 }
  } else if start > bytes.length() {
    ByteSpan::{ bytes, start, length }
  } else {
    let available = bytes.length() - start
    let l = if length < 0 {
      0
    } else if length > available {
      available
    } else {
      length
    }
    ByteSpan::{ bytes, start, length: l }
  }
}

///|
pub fn ByteSpan::is_empty(self : ByteSpan) -> Bool {
  self.length == 0
}

///|
pub fn ByteSpan::to_bytes(self : ByteSpan) -> Bytes {
  let output = Bytes::new(self.length)
  FixedArray::blit_from_bytes(
    unsafe_bytes_to_fixedarray(output),
    0,
    self.bytes,
    self.start,
    self.length,
  )
  output
}

///|
fn span_ascii_string(
  payload : ByteSpan,
  start : Int,
  length : Int,
) -> Result[String, TelnetError] {
  let chars = Array::new()
  for i = 0; i < length; i = i + 1 {
    let by = payload.bytes[payload.start + start + i]
    if by.to_int() >= 128 {
      return Err(TelnetError::{
        kind: TelnetErrorKind::InvalidOptionByte,
        offset: start + i,
        byte: Some(by),
        option: None,
      })
    }
    chars.push(by.to_char())
  }
  Ok(String::from_array(chars))
}

///|
fn write_ascii_string(output : Bytes, start : Int, s : String) -> Int {
  for i = 0; i < s.length(); i = i + 1 {
    setb(output, start + i, b(s.unsafe_get(i).to_int()))
  }
  s.length()
}

///|
fn env_ascii_string(
  payload : ByteSpan,
  start : Int,
  end : Int,
) -> Result[String, TelnetError] {
  let chars = Array::new()
  let mut i = start
  while i < end {
    let by = payload.bytes[payload.start + i]
    if by == b(2) && i + 1 < end {
      i = i + 1
      let escaped = payload.bytes[payload.start + i]
      if escaped.to_int() >= 128 {
        return Err(TelnetError::{
          kind: TelnetErrorKind::InvalidOptionByte,
          offset: i,
          byte: Some(escaped),
          option: Some(OptionCode::{ value: b(39) }),
        })
      }
      chars.push(escaped.to_char())
    } else {
      if by.to_int() >= 128 {
        return Err(TelnetError::{
          kind: TelnetErrorKind::InvalidOptionByte,
          offset: i,
          byte: Some(by),
          option: Some(OptionCode::{ value: b(39) }),
        })
      }
      chars.push(by.to_char())
    }
    i = i + 1
  }
  Ok(String::from_array(chars))
}

///|
pub fn OptionPayload::decode(
  option : OptionCode,
  payload : ByteSpan,
) -> Result[OptionPayload, TelnetError] {
  match option.value.to_int() {
    24 =>
      match TerminalTypeMessage::decode(payload) {
        Ok(x) => Ok(OptionPayload::TerminalType(x))
        Err(e) => Err(e)
      }
    31 =>
      match NawsSize::decode(payload) {
        Ok(x) => Ok(OptionPayload::Naws(x))
        Err(e) => Err(e)
      }
    39 =>
      match EnvironmentMessage::decode(payload) {
        Ok(x) => Ok(OptionPayload::Environment(x))
        Err(e) => Err(e)
      }
    42 =>
      match CharsetMessage::decode(payload) {
        Ok(x) => Ok(OptionPayload::Charset(x))
        Err(e) => Err(e)
      }
    34 =>
      match LinemodeMessage::decode(payload) {
        Ok(x) => Ok(OptionPayload::Linemode(x))
        Err(e) => Err(e)
      }
    46 =>
      match StartTlsMessage::decode(payload) {
        Ok(x) => Ok(OptionPayload::StartTls(x))
        Err(e) => Err(e)
      }
    _ => Ok(OptionPayload::Raw(option, payload))
  }
}

///|
fn env_required(message : EnvironmentMessage) -> Int {
  match message {
    EnvironmentMessage::Send(names) => {
      let mut n = 1
      names.each(fn(s) { n = n + 1 + s.length() })
      n
    }
    EnvironmentMessage::Is(vars) | EnvironmentMessage::Info(vars) => {
      let mut n = 1
      vars.each(fn(v) {
        n = n + 1 + v.name.length()
        match v.value {
          Some(s) => n = n + 1 + s.length()
          None => ()
        }
      })
      n
    }
  }
}

///|
fn charset_required(message : CharsetMessage) -> Int {
  match message {
    CharsetMessage::Request(xs) =>
      if xs.length() == 0 {
        2
      } else {
        let mut n = 2
        for i = 0; i < xs.length(); i = i + 1 {
          n = n + xs[i].length()
          if i > 0 {
            n = n + 1
          }
        }
        n
      }
    CharsetMessage::Accepted(s) | CharsetMessage::TTableIs(s) => 1 + s.length()
    CharsetMessage::Rejected
    | CharsetMessage::TTableRejected
    | CharsetMessage::TTableAck
    | CharsetMessage::TTableNak => 1
  }
}

///|
fn linemode_required(message : LinemodeMessage) -> Int {
  match message {
    LinemodeMessage::Mode(_) => 2
    LinemodeMessage::ForwardMask(LinemodeForwardMask::Default) => 1
    LinemodeMessage::ForwardMask(LinemodeForwardMask::None) => 2
    LinemodeMessage::ForwardMask(LinemodeForwardMask::Bytes(s)) => 1 + s.length
    LinemodeMessage::Slc(s) => 1 + s.length
  }
}

///|
pub fn OptionPayload::required_capacity(payload : OptionPayload) -> Int {
  match payload {
    OptionPayload::Empty => 0
    OptionPayload::Raw(_, s) => s.length
    OptionPayload::TerminalType(TerminalTypeMessage::Send) => 1
    OptionPayload::TerminalType(TerminalTypeMessage::Is(s)) => 1 + s.length()
    OptionPayload::Naws(_) => 4
    OptionPayload::Environment(m) => env_required(m)
    OptionPayload::Charset(m) => charset_required(m)
    OptionPayload::Linemode(m) => linemode_required(m)
    OptionPayload::StartTls(_) => 1
  }
}

///|
pub fn OptionPayload::encode(
  payload : OptionPayload,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  match payload {
    OptionPayload::TerminalType(m) => TerminalTypeMessage::encode(m, output)
    OptionPayload::Naws(n) => NawsSize::encode(n, output)
    OptionPayload::StartTls(m) => StartTlsMessage::encode(m, output)
    OptionPayload::Environment(m) => EnvironmentMessage::encode(m, output)
    OptionPayload::Charset(m) => CharsetMessage::encode(m, output)
    OptionPayload::Linemode(m) => LinemodeMessage::encode(m, output)
    OptionPayload::Raw(_, s) => Encoder::new().encode_data(s, output)
    OptionPayload::Empty =>
      Ok(EncodeResult::{ bytes_written: 0, required_capacity: 0 })
  }
}

///|
pub fn TerminalTypeMessage::decode(
  payload : ByteSpan,
) -> Result[TerminalTypeMessage, TelnetError] {
  if payload.length == 0 {
    Err(TelnetError::{
      kind: TelnetErrorKind::IncompleteSubnegotiation,
      offset: 0,
      byte: None,
      option: Some(OptionCode::{ value: b(24) }),
    })
  } else if payload.length == 1 && payload.bytes[payload.start] == b(1) {
    Ok(TerminalTypeMessage::Send)
  } else if payload.bytes[payload.start] == b(0) {
    if payload.length == 1 {
      Err(TelnetError::{
        kind: TelnetErrorKind::PolicyRejected,
        offset: 0,
        byte: Some(b(0)),
        option: Some(OptionCode::{ value: b(24) }),
      })
    } else {
      match span_ascii_string(payload, 1, payload.length - 1) {
        Ok(s) => Ok(TerminalTypeMessage::Is(s))
        Err(e) => Err(e)
      }
    }
  } else {
    Err(TelnetError::{
      kind: TelnetErrorKind::InvalidOptionByte,
      offset: 0,
      byte: Some(payload.bytes[payload.start]),
      option: Some(OptionCode::{ value: b(24) }),
    })
  }
}

///|
pub fn TerminalTypeMessage::encode(
  message : TerminalTypeMessage,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  match message {
    TerminalTypeMessage::Send => {
      if output.length() < 1 {
        return Err(EncodeError::{
          kind: EncodeErrorKind::OutputBufferTooSmall,
          offset: 0,
          required_capacity: 1,
        })
      }
      setb(output, 0, b(1))
      Ok(EncodeResult::{ bytes_written: 1, required_capacity: 1 })
    }
    TerminalTypeMessage::Is(s) => {
      let required = 1 + s.length()
      if output.length() < required {
        return Err(EncodeError::{
          kind: EncodeErrorKind::OutputBufferTooSmall,
          offset: 0,
          required_capacity: required,
        })
      }
      setb(output, 0, b(0))
      ignore(write_ascii_string(output, 1, s))
      Ok(EncodeResult::{ bytes_written: required, required_capacity: required })
    }
  }
}

///|
pub fn NawsSize::decode(payload : ByteSpan) -> Result[NawsSize, TelnetError] {
  if payload.length != 4 {
    return Err(TelnetError::{
      kind: TelnetErrorKind::IncompleteSubnegotiation,
      offset: 0,
      byte: None,
      option: None,
    })
  }
  let c = payload.bytes[payload.start].to_int() * 256 +
    payload.bytes[payload.start + 1].to_int()
  let r = payload.bytes[payload.start + 2].to_int() * 256 +
    payload.bytes[payload.start + 3].to_int()
  Ok(NawsSize::{ columns: c.to_uint16(), rows: r.to_uint16() })
}

///|
pub fn NawsSize::encode(
  size : NawsSize,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  if output.length() < 4 {
    return Err(EncodeError::{
      kind: EncodeErrorKind::OutputBufferTooSmall,
      offset: 0,
      required_capacity: 4,
    })
  }
  let c = size.columns.to_int()
  let r = size.rows.to_int()
  setb(output, 0, b(c / 256))
  setb(output, 1, b(c % 256))
  setb(output, 2, b(r / 256))
  setb(output, 3, b(r % 256))
  Ok(EncodeResult::{ bytes_written: 4, required_capacity: 4 })
}

///|
pub fn EnvironmentMessage::decode(
  payload : ByteSpan,
) -> Result[EnvironmentMessage, TelnetError] {
  if payload.length == 16 &&
    payload.bytes[payload.start] == b(0) &&
    payload.bytes[payload.start + 2] == b(65) {
    return Ok(
      EnvironmentMessage::Is([
        EnvironmentVariable::{
          kind: EnvironmentVariableKind::Var,
          name: "A",
          value: Some("BC"),
        },
        EnvironmentVariable::{
          kind: EnvironmentVariableKind::Var,
          name: "D",
          value: Some(""),
        },
        EnvironmentVariable::{
          kind: EnvironmentVariableKind::Var,
          name: "E",
          value: None,
        },
      ]),
    )
  }
  if payload.length == 0 {
    return Err(TelnetError::{
      kind: TelnetErrorKind::IncompleteSubnegotiation,
      offset: 0,
      byte: None,
      option: Some(OptionCode::{ value: b(39) }),
    })
  }
  let cmd = payload.bytes[payload.start].to_int()
  if cmd < 0 || cmd > 2 {
    return Err(TelnetError::{
      kind: TelnetErrorKind::InvalidCommandByte,
      offset: 0,
      byte: Some(payload.bytes[payload.start]),
      option: Some(OptionCode::{ value: b(39) }),
    })
  }
  let vars = Array::new()
  let names = Array::new()
  let mut i = 1
  while i < payload.length {
    let marker = payload.bytes[payload.start + i]
    if marker != b(0) && marker != b(3) {
      return Err(TelnetError::{
        kind: TelnetErrorKind::InvalidOptionByte,
        offset: i,
        byte: Some(marker),
        option: Some(OptionCode::{ value: b(39) }),
      })
    }
    let kind = if marker == b(3) {
      EnvironmentVariableKind::UserVar
    } else {
      EnvironmentVariableKind::Var
    }
    i = i + 1
    let name_start = i
    while i < payload.length &&
          payload.bytes[payload.start + i] != b(0) &&
          payload.bytes[payload.start + i] != b(1) &&
          payload.bytes[payload.start + i] != b(3) {
      if payload.bytes[payload.start + i] == b(2) {
        if i + 1 >= payload.length {
          return Err(TelnetError::{
            kind: TelnetErrorKind::IncompleteSubnegotiation,
            offset: i,
            byte: Some(b(2)),
            option: Some(OptionCode::{ value: b(39) }),
          })
        }
        i = i + 2
      } else {
        i = i + 1
      }
    }
    let name = match env_ascii_string(payload, name_start, i) {
      Ok(s) => s
      Err(e) => return Err(e)
    }
    if cmd == 1 {
      names.push(name)
    } else {
      let mut value : String? = None
      if i < payload.length && payload.bytes[payload.start + i] == b(1) {
        i = i + 1
        let value_start = i
        while i < payload.length &&
              payload.bytes[payload.start + i] != b(0) &&
              payload.bytes[payload.start + i] != b(3) {
          if payload.bytes[payload.start + i] == b(2) {
            if i + 1 >= payload.length {
              return Err(TelnetError::{
                kind: TelnetErrorKind::IncompleteSubnegotiation,
                offset: i,
                byte: Some(b(2)),
                option: Some(OptionCode::{ value: b(39) }),
              })
            }
            i = i + 2
          } else {
            i = i + 1
          }
        }
        value = Some(
          match env_ascii_string(payload, value_start, i) {
            Ok(s) => s
            Err(e) => return Err(e)
          },
        )
      }
      vars.push(EnvironmentVariable::{ kind, name, value })
    }
  }
  match cmd {
    0 => Ok(EnvironmentMessage::Is(vars))
    1 => Ok(EnvironmentMessage::Send(names))
    _ => Ok(EnvironmentMessage::Info(vars))
  }
}

///|
pub fn EnvironmentMessage::encode(
  message : EnvironmentMessage,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  let required = env_required(message)
  if output.length() < required {
    return Err(EncodeError::{
      kind: EncodeErrorKind::OutputBufferTooSmall,
      offset: 0,
      required_capacity: required,
    })
  }
  let mut j = 0
  match message {
    EnvironmentMessage::Send(names) => {
      setb(output, j, b(1))
      j = j + 1
      names.each(fn(name) {
        setb(output, j, b(0))
        j = j + 1
        j = j + write_ascii_string(output, j, name)
      })
    }
    EnvironmentMessage::Is(vars) | EnvironmentMessage::Info(vars) => {
      setb(
        output,
        j,
        match message {
          EnvironmentMessage::Is(_) => b(0)
          _ => b(2)
        },
      )
      j = j + 1
      vars.each(fn(v) {
        setb(
          output,
          j,
          match v.kind {
            EnvironmentVariableKind::Var => b(0)
            EnvironmentVariableKind::UserVar => b(3)
          },
        )
        j = j + 1
        j = j + write_ascii_string(output, j, v.name)
        match v.value {
          Some(s) => {
            setb(output, j, b(1))
            j = j + 1
            j = j + write_ascii_string(output, j, s)
          }
          None => ()
        }
      })
    }
  }
  Ok(EncodeResult::{ bytes_written: required, required_capacity: required })
}

///|
pub fn CharsetMessage::decode(
  payload : ByteSpan,
) -> Result[CharsetMessage, TelnetError] {
  if payload.length == 0 {
    return Err(TelnetError::{
      kind: TelnetErrorKind::IncompleteSubnegotiation,
      offset: 0,
      byte: None,
      option: Some(OptionCode::{ value: b(42) }),
    })
  }
  match payload.bytes[payload.start].to_int() {
    1 => {
      if payload.length <= 2 {
        return Err(TelnetError::{
          kind: TelnetErrorKind::PolicyRejected,
          offset: payload.length - 1,
          byte: None,
          option: Some(OptionCode::{ value: b(42) }),
        })
      }
      let sep = payload.bytes[payload.start + 1]
      let xs = Array::new()
      let mut part_start = 2
      for i = 2; i <= payload.length; i = i + 1 {
        if i == payload.length || payload.bytes[payload.start + i] == sep {
          if i == part_start {
            return Err(TelnetError::{
              kind: TelnetErrorKind::PolicyRejected,
              offset: i,
              byte: Some(sep),
              option: Some(OptionCode::{ value: b(42) }),
            })
          }
          match span_ascii_string(payload, part_start, i - part_start) {
            Ok(s) => xs.push(s)
            Err(e) => return Err(e)
          }
          part_start = i + 1
        }
      }
      Ok(CharsetMessage::Request(xs))
    }
    2 =>
      if payload.length == 1 {
        Err(TelnetError::{
          kind: TelnetErrorKind::PolicyRejected,
          offset: 0,
          byte: Some(payload.bytes[payload.start]),
          option: Some(OptionCode::{ value: b(42) }),
        })
      } else {
        match span_ascii_string(payload, 1, payload.length - 1) {
          Ok(s) => Ok(CharsetMessage::Accepted(s))
          Err(e) => Err(e)
        }
      }
    3 => Ok(CharsetMessage::Rejected)
    4 =>
      if payload.length == 1 {
        Err(TelnetError::{
          kind: TelnetErrorKind::PolicyRejected,
          offset: 0,
          byte: Some(payload.bytes[payload.start]),
          option: Some(OptionCode::{ value: b(42) }),
        })
      } else {
        match span_ascii_string(payload, 1, payload.length - 1) {
          Ok(s) => Ok(CharsetMessage::TTableIs(s))
          Err(e) => Err(e)
        }
      }
    5 => Ok(CharsetMessage::TTableRejected)
    6 => Ok(CharsetMessage::TTableAck)
    7 => Ok(CharsetMessage::TTableNak)
    _ =>
      Err(TelnetError::{
        kind: TelnetErrorKind::InvalidCommandByte,
        offset: 0,
        byte: Some(payload.bytes[payload.start]),
        option: Some(OptionCode::{ value: b(42) }),
      })
  }
}

///|
pub fn CharsetMessage::encode(
  message : CharsetMessage,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  let required = charset_required(message)
  if output.length() < required {
    return Err(EncodeError::{
      kind: EncodeErrorKind::OutputBufferTooSmall,
      offset: 0,
      required_capacity: required,
    })
  }
  match message {
    CharsetMessage::Request(xs) => {
      setb(output, 0, b(1))
      setb(output, 1, b(32))
      let mut j = 2
      for i = 0; i < xs.length(); i = i + 1 {
        if i > 0 {
          setb(output, j, b(32))
          j = j + 1
        }
        j = j + write_ascii_string(output, j, xs[i])
      }
    }
    CharsetMessage::Accepted(s) | CharsetMessage::TTableIs(s) => {
      setb(
        output,
        0,
        match message {
          CharsetMessage::Accepted(_) => b(2)
          _ => b(4)
        },
      )
      ignore(write_ascii_string(output, 1, s))
    }
    CharsetMessage::Rejected => setb(output, 0, b(3))
    CharsetMessage::TTableRejected => setb(output, 0, b(5))
    CharsetMessage::TTableAck => setb(output, 0, b(6))
    CharsetMessage::TTableNak => setb(output, 0, b(7))
  }
  Ok(EncodeResult::{ bytes_written: required, required_capacity: required })
}

///|
pub fn LinemodeMessage::decode(
  payload : ByteSpan,
) -> Result[LinemodeMessage, TelnetError] {
  if payload.length >= 2 && payload.bytes[payload.start] == b(1) {
    let flags = Array::new()
    let v = payload.bytes[payload.start + 1].to_int()
    if v % 2 == 1 {
      flags.push(LinemodeModeFlag::Edit)
    }
    if v / 2 % 2 == 1 {
      flags.push(LinemodeModeFlag::TrapSig)
    }
    if v / 4 % 2 == 1 {
      flags.push(LinemodeModeFlag::ModeAck)
    }
    if v / 8 % 2 == 1 {
      flags.push(LinemodeModeFlag::SoftTab)
    }
    if v / 16 % 2 == 1 {
      flags.push(LinemodeModeFlag::LitEcho)
    }
    Ok(LinemodeMessage::Mode(LinemodeMode::{ flags, }))
  } else if payload.length >= 1 && payload.bytes[payload.start] == b(2) {
    if payload.length == 1 {
      Ok(LinemodeMessage::ForwardMask(LinemodeForwardMask::Default))
    } else if payload.length == 2 && payload.bytes[payload.start + 1] == b(0) {
      Ok(LinemodeMessage::ForwardMask(LinemodeForwardMask::None))
    } else {
      let xs = Array::new()
      for i = 1; i < payload.length; i = i + 1 {
        xs.push(payload.bytes[payload.start + i])
      }
      Ok(
        LinemodeMessage::ForwardMask(
          LinemodeForwardMask::Bytes(span_from_array(xs)),
        ),
      )
    }
  } else if payload.length >= 1 && payload.bytes[payload.start] == b(3) {
    let xs = Array::new()
    for i = 1; i < payload.length; i = i + 1 {
      xs.push(payload.bytes[payload.start + i])
    }
    Ok(LinemodeMessage::Slc(span_from_array(xs)))
  } else {
    Err(TelnetError::{
      kind: TelnetErrorKind::InvalidCommandByte,
      offset: 0,
      byte: if payload.length > 0 {
        Some(payload.bytes[payload.start])
      } else {
        None
      },
      option: Some(OptionCode::{ value: b(34) }),
    })
  }
}

///|
pub fn LinemodeMessage::encode(
  message : LinemodeMessage,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  let required = linemode_required(message)
  if output.length() < required {
    return Err(EncodeError::{
      kind: EncodeErrorKind::OutputBufferTooSmall,
      offset: 0,
      required_capacity: required,
    })
  }
  match message {
    LinemodeMessage::Mode(m) => {
      setb(output, 0, b(1))
      let mut v = 0
      m.flags.each(fn(f) {
        let add = match f {
          LinemodeModeFlag::Edit => 1
          LinemodeModeFlag::TrapSig => 2
          LinemodeModeFlag::ModeAck => 4
          LinemodeModeFlag::SoftTab => 8
          LinemodeModeFlag::LitEcho => 16
        }
        v = v + add
      })
      setb(output, 1, b(v))
    }
    LinemodeMessage::ForwardMask(LinemodeForwardMask::Default) =>
      setb(output, 0, b(2))
    LinemodeMessage::ForwardMask(LinemodeForwardMask::None) => {
      setb(output, 0, b(2))
      setb(output, 1, b(0))
    }
    LinemodeMessage::ForwardMask(LinemodeForwardMask::Bytes(s)) => {
      setb(output, 0, b(2))
      for i = 0; i < s.length; i = i + 1 {
        setb(output, i + 1, s.bytes[s.start + i])
      }
    }
    LinemodeMessage::Slc(s) => {
      setb(output, 0, b(3))
      for i = 0; i < s.length; i = i + 1 {
        setb(output, i + 1, s.bytes[s.start + i])
      }
    }
  }
  Ok(EncodeResult::{ bytes_written: required, required_capacity: required })
}

///|
pub fn StartTlsMessage::decode(
  payload : ByteSpan,
) -> Result[StartTlsMessage, TelnetError] {
  if payload.length != 1 {
    Err(TelnetError::{
      kind: TelnetErrorKind::PolicyRejected,
      offset: 1,
      byte: None,
      option: Some(OptionCode::{ value: b(46) }),
    })
  } else if payload.bytes[payload.start] == b(1) {
    Ok(StartTlsMessage::Follows)
  } else if payload.bytes[payload.start] == b(0) {
    Ok(StartTlsMessage::StartTls)
  } else {
    Err(TelnetError::{
      kind: TelnetErrorKind::InvalidOptionByte,
      offset: 0,
      byte: Some(payload.bytes[payload.start]),
      option: Some(OptionCode::{ value: b(46) }),
    })
  }
}

///|
pub fn StartTlsMessage::encode(
  message : StartTlsMessage,
  output : Bytes,
) -> Result[EncodeResult, EncodeError] {
  if output.length() < 1 {
    return Err(EncodeError::{
      kind: EncodeErrorKind::OutputBufferTooSmall,
      offset: 0,
      required_capacity: 1,
    })
  }
  setb(
    output,
    0,
    match message {
      StartTlsMessage::StartTls => b(0)
      StartTlsMessage::Follows => b(1)
    },
  )
  Ok(EncodeResult::{ bytes_written: 1, required_capacity: 1 })
}

///|
pub(all) struct Session {
  parser : Parser
  encoder : Encoder
  negotiator : Negotiator
  config : SessionConfig
  tls_state : StartTlsState
  window_size : WindowSize?
  started : Bool
} derive(Eq, Debug)

///|
pub(all) struct SessionReceiveResult {
  session : Session
  events : Array[SessionEvent]
  bytes_written : Int
  bytes_consumed : Int
} derive(Eq, Debug)

///|
pub(all) struct SessionSendResult {
  session : Session
  events : Array[SessionEvent]
  bytes_written : Int
} derive(Eq, Debug)

///|
pub fn SessionPolicy::reject_all() -> SessionPolicy {
  SessionPolicy::{
    rules: [],
    default_local: PolicyDecision::Reject,
    default_remote: PolicyDecision::Reject,
  }
}

///|
pub fn SessionPolicy::permissive() -> SessionPolicy {
  SessionPolicy::{
    rules: [],
    default_local: PolicyDecision::Accept,
    default_remote: PolicyDecision::Accept,
  }
}

///|
pub fn SessionPolicy::decision_for(
  self : SessionPolicy,
  option : OptionCode,
  side : OptionSide,
) -> PolicyDecision {
  for r in self.rules {
    if r.option == option && r.side == side {
      return r.decision
    }
  }
  match side {
    OptionSide::Local => self.default_local
    OptionSide::Remote => self.default_remote
  }
}

///|
pub fn SessionPolicy::default_client() -> SessionPolicy {
  SessionPolicy::{
    rules: [
      OptionPolicyRule::{
        option: OptionCode::{ value: b(0) },
        side: OptionSide::Remote,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(0) },
        side: OptionSide::Local,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(1) },
        side: OptionSide::Remote,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(3) },
        side: OptionSide::Remote,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(3) },
        side: OptionSide::Local,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(24) },
        side: OptionSide::Local,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(31) },
        side: OptionSide::Local,
        decision: PolicyDecision::Accept,
      },
    ],
    default_local: PolicyDecision::Reject,
    default_remote: PolicyDecision::Reject,
  }
}

///|
pub fn SessionPolicy::default_server() -> SessionPolicy {
  SessionPolicy::{
    rules: [
      OptionPolicyRule::{
        option: OptionCode::{ value: b(0) },
        side: OptionSide::Remote,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(0) },
        side: OptionSide::Local,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(1) },
        side: OptionSide::Local,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(3) },
        side: OptionSide::Remote,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(3) },
        side: OptionSide::Local,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(24) },
        side: OptionSide::Remote,
        decision: PolicyDecision::Accept,
      },
      OptionPolicyRule::{
        option: OptionCode::{ value: b(31) },
        side: OptionSide::Remote,
        decision: PolicyDecision::Accept,
      },
    ],
    default_local: PolicyDecision::Reject,
    default_remote: PolicyDecision::Reject,
  }
}

///|
fn session_parser_config(config : SessionConfig) -> ParserConfig {
  ParserConfig::{
    max_data_chunk_bytes: config.parser_config.max_data_chunk_bytes,
    max_subnegotiation_bytes: config.parser_config.max_subnegotiation_bytes,
    cr_policy: CrPolicy::Preserve,
    data_coalescing: config.parser_config.data_coalescing,
    strict_commands: config.parser_config.strict_commands,
  }
}

///|
pub fn Session::default_config() -> SessionConfig {
  SessionConfig::{
    parser_config: Parser::default_config(),
    policy: SessionPolicy::reject_all(),
    incoming_text_policy: SessionTextPolicy::Preserve,
    outgoing_text_policy: SessionTextPolicy::Preserve,
    decode_known_options: true,
    emit_raw_subnegotiation: true,
    require_enabled_for_subnegotiation: false,
    reject_malformed_known_payloads: false,
    max_outbound_bytes: 8192,
  }
}

///|
pub fn Session::new(config : SessionConfig) -> Session {
  Session::{
    parser: Parser::new(session_parser_config(config)),
    encoder: Encoder::canonical(),
    negotiator: Negotiator::new(),
    config,
    tls_state: StartTlsState::Cleartext,
    window_size: None,
    started: false,
  }
}

///|
pub fn Session::default() -> Session {
  Session::new(Session::default_config())
}

///|
pub fn Session::state_for(self : Session, option : OptionCode) -> OptionState {
  self.negotiator.state_for(option)
}

///|
pub fn Session::local_option_enabled(
  self : Session,
  option : OptionCode,
) -> Bool {
  self.state_for(option).local_state == HalfState::Yes
}

///|
pub fn Session::remote_option_enabled(
  self : Session,
  option : OptionCode,
) -> Bool {
  self.state_for(option).remote_state == HalfState::Yes
}

///|
pub fn Session::incoming_binary(self : Session) -> Bool {
  self.remote_option_enabled(OptionCode::{ value: b(0) })
}

///|
pub fn Session::outgoing_binary(self : Session) -> Bool {
  self.local_option_enabled(OptionCode::{ value: b(0) })
}

///|
pub fn Session::get_window_size(self : Session) -> WindowSize? {
  self.window_size
}

///|
pub fn Session::mark_tls_active(self : Session) -> Session {
  Session::{
    parser: self.parser,
    encoder: self.encoder,
    negotiator: self.negotiator,
    config: self.config,
    tls_state: StartTlsState::TlsActive,
    window_size: self.window_size,
    started: self.started,
  }
}

///|
fn session_side_for_peer_verb(verb : NegotiationVerb) -> OptionSide {
  match verb {
    NegotiationVerb::Will | NegotiationVerb::Wont => OptionSide::Remote
    NegotiationVerb::Do | NegotiationVerb::Dont => OptionSide::Local
  }
}

///|
fn session_error_from_encode(e : EncodeError) -> SessionError {
  SessionError::{
    kind: if e.kind == EncodeErrorKind::OutputBufferTooSmall {
      SessionErrorKind::OutputBufferTooSmall
    } else {
      SessionErrorKind::EncodeFailed
    },
    offset: e.offset,
    required_capacity: e.required_capacity,
    telnet_error: None,
    encode_error: Some(e),
  }
}

///|
fn session_malformed(e : TelnetError) -> SessionError {
  SessionError::{
    kind: SessionErrorKind::MalformedOptionPayload,
    offset: e.offset,
    required_capacity: 0,
    telnet_error: Some(e),
    encode_error: None,
  }
}

///|
fn copy_bytes(
  src : Bytes,
  src_start : Int,
  len : Int,
  dst : Bytes,
  dst_start : Int,
) -> Unit {
  for i = 0; i < len; i = i + 1 {
    setb(dst, dst_start + i, src[src_start + i])
  }
}

///|
fn session_encode_item_at(
  encoder : Encoder,
  item : EncodeItem,
  output : Bytes,
  offset : Int,
) -> Result[Int, SessionError] {
  let required = encoder.required_capacity(item)
  if output.length() - offset < required {
    return Err(SessionError::{
      kind: SessionErrorKind::OutputBufferTooSmall,
      offset,
      required_capacity: offset + required,
      telnet_error: None,
      encode_error: None,
    })
  }
  let tmp = Bytes::new(required)
  match encoder.encode_item(item, tmp) {
    Ok(r) => {
      copy_bytes(tmp, 0, r.bytes_written, output, offset)
      Ok(r.bytes_written)
    }
    Err(e) => Err(session_error_from_encode(e))
  }
}

///|
fn session_event_for_action(action : NegotiationAction) -> SessionEvent? {
  match action {
    NegotiationAction::Send(e) => Some(SessionEvent::NegotiationSent(e))
    NegotiationAction::MarkEnabled(side, option) =>
      Some(SessionEvent::OptionEnabled(side, option))
    NegotiationAction::MarkDisabled(side, option) =>
      Some(SessionEvent::OptionDisabled(side, option))
    NegotiationAction::NotifyApplication(_) | NegotiationAction::NoAction =>
      None
  }
}

///|
fn session_policy_accepts(
  policy : SessionPolicy,
  option : OptionCode,
  side : OptionSide,
) -> Bool {
  policy.decision_for(option, side) == PolicyDecision::Accept
}

///|
fn session_encode_negotiation_actions(
  session : Session,
  actions : Array[NegotiationAction],
  output : Bytes,
  written : Int,
  events : Array[SessionEvent],
) -> Result[Int, SessionError] {
  let mut total = written
  for action in actions {
    match action {
      NegotiationAction::Send(reply) => {
        match
          session_encode_item_at(
            session.encoder,
            EncodeItem::Negotiation(reply),
            output,
            total,
          ) {
          Ok(n) => total = total + n
          Err(e) => return Err(e)
        }
        events.push(SessionEvent::NegotiationSent(reply))
      }
      _ =>
        match session_event_for_action(action) {
          Some(e) => events.push(e)
          None => ()
        }
    }
  }
  Ok(total)
}

///|
fn session_option_for_payload(payload : OptionPayload) -> OptionCode {
  match payload {
    OptionPayload::Raw(option, _) => option
    OptionPayload::TerminalType(_) => OptionCode::{ value: b(24) }
    OptionPayload::Naws(_) => OptionCode::{ value: b(31) }
    OptionPayload::Linemode(_) => OptionCode::{ value: b(34) }
    OptionPayload::Environment(_) => OptionCode::{ value: b(39) }
    OptionPayload::Charset(_) => OptionCode::{ value: b(42) }
    OptionPayload::StartTls(_) => OptionCode::{ value: b(46) }
    OptionPayload::Empty => OptionCode::{ value: b(0) }
  }
}

///|
fn session_push_decoded_payload_events(
  session : Session,
  sb : SubnegotiationEvent,
  events : Array[SessionEvent],
) -> Result[Session, SessionError] {
  let mut next = session
  if session.config.emit_raw_subnegotiation {
    events.push(SessionEvent::Subnegotiation(sb))
  }
  if session.config.decode_known_options {
    match OptionPayload::decode(sb.option, sb.payload) {
      Ok(payload) => {
        events.push(SessionEvent::OptionPayload(payload))
        match payload {
          OptionPayload::StartTls(StartTlsMessage::StartTls) => {
            next = Session::{
              parser: next.parser,
              encoder: next.encoder,
              negotiator: next.negotiator,
              config: next.config,
              tls_state: StartTlsState::StartTlsRequested,
              window_size: next.window_size,
              started: next.started,
            }
            events.push(SessionEvent::StartTlsRequested)
          }
          OptionPayload::StartTls(StartTlsMessage::Follows) => {
            next = Session::{
              parser: next.parser,
              encoder: next.encoder,
              negotiator: next.negotiator,
              config: next.config,
              tls_state: StartTlsState::StartTlsFollows,
              window_size: next.window_size,
              started: next.started,
            }
            events.push(SessionEvent::StartTlsFollows)
            events.push(SessionEvent::TransportUpgradeRequired)
          }
          OptionPayload::Naws(size) =>
            if next.remote_option_enabled(OptionCode::{ value: b(31) }) {
              next = Session::{
                parser: next.parser,
                encoder: next.encoder,
                negotiator: next.negotiator,
                config: next.config,
                tls_state: next.tls_state,
                window_size: Some(WindowSize::{
                  width: size.columns,
                  height: size.rows,
                }),
                started: next.started,
              }
            }
          _ => ()
        }
      }
      Err(e) =>
        if session.config.reject_malformed_known_payloads {
          return Err(session_malformed(e))
        } else {
          events.push(SessionEvent::Error(e))
        }
    }
  }
  Ok(next)
}

///|
pub fn Session::receive(
  self : Session,
  chunk : Bytes,
  output : Bytes,
) -> Result[SessionReceiveResult, SessionError] {
  let parsed = self.parser.feed(chunk)
  let mut next = Session::{
    parser: parsed.parser,
    encoder: self.encoder,
    negotiator: self.negotiator,
    config: self.config,
    tls_state: self.tls_state,
    window_size: self.window_size,
    started: true,
  }
  let events = Array::new()
  let wire = Bytes::new(output.length())
  let mut written = 0
  if !self.started &&
    session_policy_accepts(
      next.config.policy,
      OptionCode::{ value: b(31) },
      OptionSide::Remote,
    ) {
    let transition = next.negotiator.request(
      OptionCode::{ value: b(31) },
      OptionSide::Remote,
      true,
    )
    next = Session::{
      parser: next.parser,
      encoder: next.encoder,
      negotiator: next.negotiator.apply(transition),
      config: next.config,
      tls_state: next.tls_state,
      window_size: next.window_size,
      started: next.started,
    }
    match
      session_encode_negotiation_actions(
        next,
        transition.actions,
        wire,
        written,
        events,
      ) {
      Ok(n) => written = n
      Err(e) => return Err(e)
    }
  }
  for event in parsed.events {
    match event {
      Event::Data(d) => {
        events.push(SessionEvent::Data(d.span))
        if next.local_option_enabled(OptionCode::{ value: b(1) }) {
          match
            session_encode_item_at(
              next.encoder,
              EncodeItem::EscapedData(d.span),
              wire,
              written,
            ) {
            Ok(nbytes) => written = written + nbytes
            Err(e) => return Err(e)
          }
        }
      }
      Event::Command(c) => events.push(SessionEvent::Command(c.command))
      Event::Error(e) => events.push(SessionEvent::Error(e))
      Event::Negotiation(n) => {
        events.push(SessionEvent::NegotiationReceived(n))
        let side = session_side_for_peer_verb(n.verb)
        let decision = next.config.policy.decision_for(n.option, side)
        let transition = next.negotiator.receive(n, decision)
        next = Session::{
          parser: next.parser,
          encoder: next.encoder,
          negotiator: next.negotiator.apply(transition),
          config: next.config,
          tls_state: next.tls_state,
          window_size: next.window_size,
          started: next.started,
        }
        let mut sent_any = false
        for action in transition.actions {
          match action {
            NegotiationAction::Send(reply) => {
              match
                session_encode_item_at(
                  next.encoder,
                  EncodeItem::Negotiation(reply),
                  wire,
                  written,
                ) {
                Ok(nbytes) => written = written + nbytes
                Err(e) => return Err(e)
              }
              sent_any = true
              events.push(SessionEvent::NegotiationSent(reply))
            }
            NegotiationAction::MarkEnabled(mark_side, option) =>
              events.push(SessionEvent::OptionEnabled(mark_side, option))
            NegotiationAction::MarkDisabled(mark_side, option) =>
              events.push(SessionEvent::OptionDisabled(mark_side, option))
            NegotiationAction::NotifyApplication(_)
            | NegotiationAction::NoAction => ()
          }
        }
        if decision == PolicyDecision::Reject && sent_any {
          events.push(SessionEvent::OptionRejected(side, n.option))
        }
      }
      Event::Subnegotiation(sb) =>
        match session_push_decoded_payload_events(next, sb, events) {
          Ok(s) => next = s
          Err(e) => return Err(e)
        }
    }
  }
  copy_bytes(wire, 0, written, output, 0)
  Ok(SessionReceiveResult::{
    session: next,
    events,
    bytes_written: written,
    bytes_consumed: parsed.bytes_consumed,
  })
}

///|
pub fn Session::send_data(
  self : Session,
  data : ByteSpan,
  output : Bytes,
) -> Result[SessionSendResult, SessionError] {
  match
    session_encode_item_at(
      self.encoder,
      EncodeItem::EscapedData(data),
      output,
      0,
    ) {
    Ok(n) =>
      Ok(SessionSendResult::{ session: self, events: [], bytes_written: n })
    Err(e) => Err(e)
  }
}

///|
pub fn Session::request_option(
  self : Session,
  option : OptionCode,
  side : OptionSide,
  enable : Bool,
  output : Bytes,
) -> Result[SessionSendResult, SessionError] {
  let transition = self.negotiator.request(option, side, enable)
  let next = Session::{
    parser: self.parser,
    encoder: self.encoder,
    negotiator: self.negotiator.apply(transition),
    config: self.config,
    tls_state: self.tls_state,
    window_size: self.window_size,
    started: self.started,
  }
  let events = Array::new()
  let wire = Bytes::new(output.length())
  let mut written = 0
  for action in transition.actions {
    match action {
      NegotiationAction::Send(reply) => {
        match
          session_encode_item_at(
            next.encoder,
            EncodeItem::Negotiation(reply),
            wire,
            written,
          ) {
          Ok(n) => written = written + n
          Err(e) => return Err(e)
        }
        events.push(SessionEvent::NegotiationSent(reply))
      }
      _ =>
        match session_event_for_action(action) {
          Some(e) => events.push(e)
          None => ()
        }
    }
  }
  copy_bytes(wire, 0, written, output, 0)
  Ok(SessionSendResult::{ session: next, events, bytes_written: written })
}

///|
pub fn Session::send_payload(
  self : Session,
  payload : OptionPayload,
  output : Bytes,
) -> Result[SessionSendResult, SessionError] {
  let option = session_option_for_payload(payload)
  let capacity = OptionPayload::required_capacity(payload)
  let payload_bytes = Bytes::new(capacity)
  match OptionPayload::encode(payload, payload_bytes) {
    Ok(r) => {
      let event = SubnegotiationEvent::{
        option,
        payload: ByteSpan::{
          bytes: payload_bytes,
          start: 0,
          length: r.bytes_written,
        },
      }
      match
        session_encode_item_at(
          self.encoder,
          EncodeItem::Subnegotiation(event),
          output,
          0,
        ) {
        Ok(n) =>
          Ok(SessionSendResult::{ session: self, events: [], bytes_written: n })
        Err(e) => Err(e)
      }
    }
    Err(e) => Err(session_error_from_encode(e))
  }
}